diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentNavigation.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentNavigation.kt index b16db4b3f35..097b6275c1b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentNavigation.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentNavigation.kt @@ -1,6 +1,5 @@ package com.woocommerce.android.ui.woopos.cashpayment -import androidx.compose.animation.core.spring import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.navigation.NavController @@ -8,38 +7,32 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument +import com.woocommerce.android.ui.woopos.home.HOME_ROUTE import com.woocommerce.android.ui.woopos.root.navigation.WooPosNavigationEvent -private const val ROUTE = "cash_payment/{orderId}" +const val CASH_ROUTE_ORDER_ID_KEY = "orderId" +private const val CASH_ROUTE = "$HOME_ROUTE/cash_payment/{$CASH_ROUTE_ORDER_ID_KEY}" fun NavController.navigateToCashPaymentScreen(orderId: Long) { - navigate("cash_payment/$orderId") + navigate(CASH_ROUTE.replace("{$CASH_ROUTE_ORDER_ID_KEY}", orderId.toString())) } fun NavGraphBuilder.cashPaymentScreen( onNavigationEvent: (WooPosNavigationEvent) -> Unit ) { composable( - route = ROUTE, + route = CASH_ROUTE, arguments = listOf( - navArgument("orderId") { type = NavType.LongType } + navArgument(CASH_ROUTE_ORDER_ID_KEY) { type = NavType.LongType } ), enterTransition = { slideInHorizontally( initialOffsetX = { fullWidth -> fullWidth }, - animationSpec = spring( - dampingRatio = 0.8f, - stiffness = 200f - ) ) }, exitTransition = { slideOutHorizontally( targetOffsetX = { fullWidth -> fullWidth }, - animationSpec = spring( - dampingRatio = 0.8f, - stiffness = 200f - ) ) }, ) { backStackEntry -> diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentRepository.kt index 9907e65f4b9..d85c3c5cc71 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentRepository.kt @@ -1,9 +1,17 @@ package com.woocommerce.android.ui.woopos.cashpayment +import com.woocommerce.android.model.Order import com.woocommerce.android.model.OrderMapper import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.util.WooLog +import com.woocommerce.android.util.WooLog.T import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext +import org.wordpress.android.fluxc.model.WCOrderStatusModel +import org.wordpress.android.fluxc.store.WCGatewayStore import org.wordpress.android.fluxc.store.WCOrderStore import javax.inject.Inject @@ -11,10 +19,43 @@ class WooPosCashPaymentRepository @Inject constructor( private val selectedSite: SelectedSite, private val orderStore: WCOrderStore, private val orderMapper: OrderMapper, + private val gatewayStore: WCGatewayStore, ) { suspend fun getOrderById(orderId: Long) = withContext(Dispatchers.IO) { orderStore.getOrderByIdAndSite(orderId, selectedSite.get())?.let { orderMapper.toAppModel(it) } } + + suspend fun completeOrder(orderId: Long): Result = withContext(Dispatchers.IO) { + val codGateway = gatewayStore.getGateway(selectedSite.get(), CASH_ON_DELIVERY_PAYMENT_TYPE) + + val statusModel = orderStore.getOrderStatusForSiteAndKey( + selectedSite.get(), + Order.Status.Completed.value + ) ?: WCOrderStatusModel(statusKey = Order.Status.Completed.value).apply { + label = statusKey + } + + orderStore.updateOrderStatusAndPaymentMethod( + orderId = orderId, + site = selectedSite.get(), + newStatus = statusModel, + newPaymentMethodId = CASH_ON_DELIVERY_PAYMENT_TYPE, + codGateway?.title ?: "Pay in Person", + ) + .filterIsInstance() + .map { result -> + if (result.event.isError) { + WooLog.e(T.POS, "Order completion failed - ${result.event.error.message}") + Result.failure(Exception(result.event.error.message)) + } else { + Result.success(Unit) + } + }.first() + } + + private companion object { + const val CASH_ON_DELIVERY_PAYMENT_TYPE = "cod" + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentScreen.kt index 0247ca2a88d..2e4e8f8363d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentScreen.kt @@ -41,6 +41,7 @@ fun WooPosCashPaymentScreen(onNavigationEvent: (WooPosNavigationEvent) -> Unit) onAmountChanged = { viewModel.onUIEvent(WooPosCashPaymentUIEvent.AmountChanged(it)) }, onCompleteOrderClicked = { viewModel.onUIEvent(WooPosCashPaymentUIEvent.CompleteOrderClicked) }, onBackClicked = { onNavigationEvent(WooPosNavigationEvent.BackFromCashPayment) }, + onOrderComplete = { onNavigationEvent(WooPosNavigationEvent.OpenHomeFromCashPaymentAfterSuccessfulPayment) }, ) } @@ -50,6 +51,7 @@ fun WooPosCashPaymentScreen( onAmountChanged: (String) -> Unit, onCompleteOrderClicked: () -> Unit, onBackClicked: () -> Unit, + onOrderComplete: () -> Unit, ) { Column( modifier = Modifier @@ -66,7 +68,7 @@ fun WooPosCashPaymentScreen( ) } - WooPosCashPaymentState.Finishing -> TODO() + WooPosCashPaymentState.Complete -> onOrderComplete() WooPosCashPaymentState.Initiating -> { // show nothing } @@ -86,6 +88,7 @@ private fun Collecting( Column( modifier = Modifier .width(540.dp) + .padding(32.dp) ) { Spacer(modifier = Modifier.weight(1f)) @@ -133,9 +136,9 @@ private fun Collecting( Spacer(modifier = Modifier.height(16.dp)) WooPosButton( - text = "Mark completed", + text = state.button.text, onClick = onCompleteOrderClicked, - enabled = state.canBeOrderBeCompleted, + enabled = state.button.status == WooPosCashPaymentState.Collecting.Button.Status.ENABLED, ) Spacer(modifier = Modifier.weight(1f)) @@ -196,11 +199,15 @@ fun WooPosTotalsPaymentCashScreenScreen() { enteredAmount = "5$", changeDue = "5$", total = "10$", - canBeOrderBeCompleted = true, + button = WooPosCashPaymentState.Collecting.Button( + text = "Complete order", + status = WooPosCashPaymentState.Collecting.Button.Status.ENABLED + ) ), onAmountChanged = {}, onCompleteOrderClicked = {}, onBackClicked = {}, + onOrderComplete = {}, ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentState.kt index 0e073f1e5f8..6bc80795122 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentState.kt @@ -9,10 +9,23 @@ sealed class WooPosCashPaymentState : Parcelable { val enteredAmount: String, val changeDue: String, val total: String, - val canBeOrderBeCompleted: Boolean, - ) : WooPosCashPaymentState() + val button: Button + ) : WooPosCashPaymentState() { + @Parcelize + data class Button( + val text: String, + val status: Status, + ) : Parcelable { + @Parcelize + enum class Status : Parcelable { + ENABLED, + DISABLED, + LOADING + } + } + } - object Finishing : WooPosCashPaymentState() + object Complete : WooPosCashPaymentState() object Initiating : WooPosCashPaymentState() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentViewModel.kt index cbb65070287..6f608451a5a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentViewModel.kt @@ -3,7 +3,9 @@ package com.woocommerce.android.ui.woopos.cashpayment import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.woocommerce.android.R import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice +import com.woocommerce.android.viewmodel.ResourceProvider import com.woocommerce.android.viewmodel.getStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow @@ -15,9 +17,10 @@ import javax.inject.Inject class WooPosCashPaymentViewModel @Inject constructor( private val repository: WooPosCashPaymentRepository, private val priceFormat: WooPosFormatPrice, + private val resourceProvider: ResourceProvider, savedState: SavedStateHandle, ) : ViewModel() { - private val orderId = savedState.get("orderId")!! + private val orderId = savedState.get(CASH_ROUTE_ORDER_ID_KEY)!! private val _state = savedState.getStateFlow( scope = viewModelScope, @@ -34,7 +37,10 @@ class WooPosCashPaymentViewModel @Inject constructor( enteredAmount = "", changeDue = priceFormat(BigDecimal.ZERO), total = priceFormat(order.total), - canBeOrderBeCompleted = false + button = WooPosCashPaymentState.Collecting.Button( + text = resourceProvider.getString(R.string.woopos_complete_cash_order_button), + status = WooPosCashPaymentState.Collecting.Button.Status.ENABLED + ) ) } } @@ -42,7 +48,31 @@ class WooPosCashPaymentViewModel @Inject constructor( fun onUIEvent(event: WooPosCashPaymentUIEvent) { when (event) { is WooPosCashPaymentUIEvent.AmountChanged -> TODO() - WooPosCashPaymentUIEvent.CompleteOrderClicked -> TODO() + WooPosCashPaymentUIEvent.CompleteOrderClicked -> handleOrderCompletion() + } + } + + private fun handleOrderCompletion() { + viewModelScope.launch { + val stateBeforeCompleting = _state.value as? WooPosCashPaymentState.Collecting ?: return@launch + _state.value = stateBeforeCompleting.copy( + button = stateBeforeCompleting.button.copy( + status = WooPosCashPaymentState.Collecting.Button.Status.LOADING + ) + ) + + val result = repository.completeOrder(orderId) + if (result.isSuccess) { + _state.value = WooPosCashPaymentState.Complete + } else { + val currentState = _state.value as? WooPosCashPaymentState.Collecting ?: return@launch + currentState + _state.value = currentState.copy( + button = currentState.button.copy( + status = WooPosCashPaymentState.Collecting.Button.Status.ENABLED + ) + ) + } } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeNavigation.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeNavigation.kt index 29ae79ba112..7dd2cc6e583 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeNavigation.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeNavigation.kt @@ -1,21 +1,30 @@ package com.woocommerce.android.ui.woopos.home import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.woocommerce.android.ui.woopos.root.navigation.WooPosNavigationEvent -private const val HOME_ROUTE = "home" +const val HOME_ROUTE = "home" +const val HOME_PAYMENT_COMPLETED_VIA_CASH_KEY = "home_payment_completed_via_cash_key" fun NavController.navigateToHomeScreen() { navigate(HOME_ROUTE) } +fun NavController.navigateToHomeScreenAfterSuccessfulCashPayment() { + previousBackStackEntry + ?.savedStateHandle + ?.set(HOME_PAYMENT_COMPLETED_VIA_CASH_KEY, true) + + popBackStack() +} + fun NavGraphBuilder.homeScreen( onNavigationEvent: (WooPosNavigationEvent) -> Unit ) { @@ -33,13 +42,23 @@ fun NavGraphBuilder.homeScreen( exitTransition = { slideOutHorizontally( targetOffsetX = { fullWidth -> -fullWidth }, - animationSpec = spring( - dampingRatio = 0.8f, - stiffness = 200f - ) ) }, - ) { - WooPosHomeScreen(onNavigationEvent) + popEnterTransition = { + slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth }, + ) + }, + ) { entry -> + val savedStateHandle = entry.savedStateHandle + val isPaymentCompletedViaCash = savedStateHandle.get(HOME_PAYMENT_COMPLETED_VIA_CASH_KEY) == true + if (isPaymentCompletedViaCash) { + savedStateHandle[HOME_PAYMENT_COMPLETED_VIA_CASH_KEY] = false + } + + WooPosHomeScreen( + isPaymentCompletedViaCash = isPaymentCompletedViaCash, + onNavigationEvent = onNavigationEvent, + ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeScreen.kt index 3c607d4a8ed..f209bdf1e68 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeScreen.kt @@ -42,15 +42,25 @@ import com.woocommerce.android.ui.woopos.home.toolbar.WooPosFloatingToolbar import com.woocommerce.android.ui.woopos.home.totals.WooPosTotalsScreen import com.woocommerce.android.ui.woopos.home.totals.WooPosTotalsScreenPreview import com.woocommerce.android.ui.woopos.root.navigation.WooPosNavigationEvent +import kotlinx.coroutines.delay import org.wordpress.android.util.ToastUtils @Composable fun WooPosHomeScreen( + isPaymentCompletedViaCash: Boolean, onNavigationEvent: (WooPosNavigationEvent) -> Unit ) { val viewModel: WooPosHomeViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value val context = LocalContext.current + + LaunchedEffect(Unit) { + if (isPaymentCompletedViaCash) { + delay(800) + viewModel.onUIEvent(WooPosHomeUIEvent.OnPaymentCompletedViaCash) + } + } + LaunchedEffect(viewModel.toastEvent) { viewModel.toastEvent.collect { message -> ToastUtils.showToast( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeUIEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeUIEvent.kt index 0c5fdbeeb7b..47647b51cee 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeUIEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeUIEvent.kt @@ -4,4 +4,5 @@ sealed class WooPosHomeUIEvent { data object SystemBackClicked : WooPosHomeUIEvent() data object ExitConfirmationDialogDismissed : WooPosHomeUIEvent() data object DismissProductsInfoDialog : WooPosHomeUIEvent() + data object OnPaymentCompletedViaCash : WooPosHomeUIEvent() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt index f8204e3d626..f68e85ee473 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt @@ -3,6 +3,9 @@ package com.woocommerce.android.ui.woopos.home import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.woocommerce.android.ui.woopos.home.WooPosHomeState.ExitConfirmationDialog +import com.woocommerce.android.ui.woopos.home.WooPosHomeState.ProductsInfoDialog +import com.woocommerce.android.ui.woopos.home.WooPosHomeState.ScreenPositionState import com.woocommerce.android.viewmodel.getStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow @@ -21,9 +24,9 @@ class WooPosHomeViewModel @Inject constructor( scope = viewModelScope, key = "home_state", initialValue = WooPosHomeState( - screenPositionState = WooPosHomeState.ScreenPositionState.Cart.Visible, - productsInfoDialog = WooPosHomeState.ProductsInfoDialog(isVisible = false), - exitConfirmationDialog = WooPosHomeState.ExitConfirmationDialog(isVisible = false), + screenPositionState = ScreenPositionState.Cart.Visible, + productsInfoDialog = ProductsInfoDialog(isVisible = false), + exitConfirmationDialog = ExitConfirmationDialog(isVisible = false), ) ) val state: StateFlow = _state @@ -39,23 +42,22 @@ class WooPosHomeViewModel @Inject constructor( return when (event) { WooPosHomeUIEvent.SystemBackClicked -> { when (_state.value.screenPositionState) { - WooPosHomeState.ScreenPositionState.Checkout.NotPaid -> { + ScreenPositionState.Checkout.NotPaid -> { _state.value = _state.value.copy( - screenPositionState = WooPosHomeState.ScreenPositionState.Cart.Visible + screenPositionState = ScreenPositionState.Cart.Visible ) sendEventToChildren(ParentToChildrenEvent.BackFromCheckoutToCartClicked) } - WooPosHomeState.ScreenPositionState.Checkout.Paid -> { + ScreenPositionState.Checkout.Paid -> { _state.value = _state.value.copy( - screenPositionState = WooPosHomeState.ScreenPositionState.Cart.Visible + screenPositionState = ScreenPositionState.Cart.Visible ) - sendEventToChildren(ParentToChildrenEvent.OrderSuccessfullyPaid) } - is WooPosHomeState.ScreenPositionState.Cart -> { + is ScreenPositionState.Cart -> { _state.value = _state.value.copy( - exitConfirmationDialog = WooPosHomeState.ExitConfirmationDialog(isVisible = true) + exitConfirmationDialog = ExitConfirmationDialog(isVisible = true) ) } } @@ -63,15 +65,17 @@ class WooPosHomeViewModel @Inject constructor( WooPosHomeUIEvent.ExitConfirmationDialogDismissed -> { _state.value = _state.value.copy( - exitConfirmationDialog = WooPosHomeState.ExitConfirmationDialog(isVisible = false) + exitConfirmationDialog = ExitConfirmationDialog(isVisible = false) ) } WooPosHomeUIEvent.DismissProductsInfoDialog -> { _state.value = _state.value.copy( - productsInfoDialog = WooPosHomeState.ProductsInfoDialog(isVisible = false) + productsInfoDialog = ProductsInfoDialog(isVisible = false) ) } + + WooPosHomeUIEvent.OnPaymentCompletedViaCash -> onOrderSuccessfullyPaid() } } @@ -81,14 +85,14 @@ class WooPosHomeViewModel @Inject constructor( when (event) { is ChildToParentEvent.CheckoutClicked -> { _state.value = _state.value.copy( - screenPositionState = WooPosHomeState.ScreenPositionState.Checkout.NotPaid + screenPositionState = ScreenPositionState.Checkout.NotPaid ) sendEventToChildren(ParentToChildrenEvent.CheckoutClicked(event.itemClickedDataList)) } is ChildToParentEvent.BackFromCheckoutToCartClicked -> { _state.value = _state.value.copy( - screenPositionState = WooPosHomeState.ScreenPositionState.Cart.Visible + screenPositionState = ScreenPositionState.Cart.Visible ) } @@ -100,20 +104,15 @@ class WooPosHomeViewModel @Inject constructor( is ChildToParentEvent.NewTransactionClicked -> { _state.value = _state.value.copy( - screenPositionState = WooPosHomeState.ScreenPositionState.Cart.Visible + screenPositionState = ScreenPositionState.Cart.Visible ) - sendEventToChildren(ParentToChildrenEvent.OrderSuccessfullyPaid) } - is ChildToParentEvent.OrderSuccessfullyPaid -> { - _state.value = _state.value.copy( - screenPositionState = WooPosHomeState.ScreenPositionState.Checkout.Paid - ) - } + is ChildToParentEvent.OrderSuccessfullyPaid -> onOrderSuccessfullyPaid() ChildToParentEvent.ExitPosClicked -> { _state.value = _state.value.copy( - exitConfirmationDialog = WooPosHomeState.ExitConfirmationDialog(isVisible = true) + exitConfirmationDialog = ExitConfirmationDialog(isVisible = true) ) } @@ -121,7 +120,7 @@ class WooPosHomeViewModel @Inject constructor( ChildToParentEvent.ProductsDialogInfoIconClicked -> { _state.value = _state.value.copy( - productsInfoDialog = WooPosHomeState.ProductsInfoDialog(isVisible = true) + productsInfoDialog = ProductsInfoDialog(isVisible = true) ) } @@ -140,18 +139,18 @@ class WooPosHomeViewModel @Inject constructor( val newScreenPositionState = when (event) { ChildToParentEvent.ProductsStatusChanged.FullScreen -> { when (screenPosition) { - is WooPosHomeState.ScreenPositionState.Cart -> WooPosHomeState.ScreenPositionState.Cart.Hidden - is WooPosHomeState.ScreenPositionState.Checkout -> screenPosition + is ScreenPositionState.Cart -> ScreenPositionState.Cart.Hidden + is ScreenPositionState.Checkout -> screenPosition } } ChildToParentEvent.ProductsStatusChanged.WithCart -> { when (screenPosition) { - WooPosHomeState.ScreenPositionState.Cart.Hidden -> - WooPosHomeState.ScreenPositionState.Cart.Visible + ScreenPositionState.Cart.Hidden -> + ScreenPositionState.Cart.Visible - WooPosHomeState.ScreenPositionState.Cart.Visible, - WooPosHomeState.ScreenPositionState.Checkout.NotPaid, - WooPosHomeState.ScreenPositionState.Checkout.Paid -> screenPosition + ScreenPositionState.Cart.Visible, + ScreenPositionState.Checkout.NotPaid, + ScreenPositionState.Checkout.Paid -> screenPosition } } } @@ -163,4 +162,11 @@ class WooPosHomeViewModel @Inject constructor( parentToChildrenEventSender.sendToChildren(event) } } + + private fun onOrderSuccessfullyPaid() { + _state.value = _state.value.copy( + screenPositionState = ScreenPositionState.Checkout.Paid + ) + sendEventToChildren(ParentToChildrenEvent.OrderSuccessfullyPaid) + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt index d3e2b706766..af526d3c26f 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt @@ -103,7 +103,7 @@ class WooPosTotalsViewModel @Inject constructor( WooPosTotalsUIEvent.OnStartReceiptFlowClicked -> { viewModelScope.launch { if (isReceiptSendingSupportedValue.await()) { - uiState.value = WooPosTotalsViewState.ReceiptSending(email = "") + uiState.value = ReceiptSending(email = "") } else { childrenToParentEventSender.sendToParent( ChildToParentEvent.ToastMessageDisplayed( @@ -161,8 +161,9 @@ class WooPosTotalsViewModel @Inject constructor( uiState.value = InitialState } - is ParentToChildrenEvent.ItemClickedInProductSelector, - ParentToChildrenEvent.OrderSuccessfullyPaid -> Unit + ParentToChildrenEvent.OrderSuccessfullyPaid -> showSuccessfulPaymentState() + + is ParentToChildrenEvent.ItemClickedInProductSelector -> Unit } } } @@ -173,16 +174,6 @@ class WooPosTotalsViewModel @Inject constructor( cardReaderFacade.paymentStatus.collect { status -> when (status) { is WooPosCardReaderPaymentStatus.Success -> { - val state = uiState.value - check(state is WooPosTotalsViewState.Totals) - val orderTotalText = resourceProvider.getString( - R.string.woopos_success_screen_total, - state.orderTotalText - ) - uiState.value = WooPosTotalsViewState.PaymentSuccess( - orderTotalText = orderTotalText, - isReceiptAvailable = isReceiptsEnabled() - ) childrenToParentEventSender.sendToParent(ChildToParentEvent.OrderSuccessfullyPaid) } is WooPosCardReaderPaymentStatus.Failure, @@ -223,6 +214,21 @@ class WooPosTotalsViewModel @Inject constructor( } } + private fun showSuccessfulPaymentState() { + viewModelScope.launch { + val state = uiState.value + check(state is WooPosTotalsViewState.Totals) + val orderTotalText = resourceProvider.getString( + R.string.woopos_success_screen_total, + state.orderTotalText + ) + uiState.value = WooPosTotalsViewState.PaymentSuccess( + orderTotalText = orderTotalText, + isReceiptAvailable = isReceiptsEnabled() + ) + } + } + private suspend fun buildWooPosTotalsViewState(order: Order): WooPosTotalsViewState.Totals { val subtotalAmount = order.productsTotal val taxAmount = order.totalTax diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEvent.kt index 1c95f07a543..60e2c69b10d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEvent.kt @@ -6,4 +6,5 @@ sealed class WooPosNavigationEvent { data object OpenHomeFromSplash : WooPosNavigationEvent() data class OpenCashPayment(val orderId: Long) : WooPosNavigationEvent() data object BackFromCashPayment : WooPosNavigationEvent() + data object OpenHomeFromCashPaymentAfterSuccessfulPayment : WooPosNavigationEvent() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEventHandler.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEventHandler.kt index 24a35612436..29f9e16562d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEventHandler.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEventHandler.kt @@ -4,6 +4,7 @@ import androidx.activity.ComponentActivity import androidx.navigation.NavHostController import com.woocommerce.android.ui.woopos.cashpayment.navigateToCashPaymentScreen import com.woocommerce.android.ui.woopos.home.navigateToHomeScreen +import com.woocommerce.android.ui.woopos.home.navigateToHomeScreenAfterSuccessfulCashPayment fun NavHostController.handleNavigationEvent( event: WooPosNavigationEvent, @@ -15,6 +16,8 @@ fun NavHostController.handleNavigationEvent( is WooPosNavigationEvent.OpenHomeFromSplash -> navigateToHomeScreen() is WooPosNavigationEvent.OpenCashPayment -> navigateToCashPaymentScreen(event.orderId) - WooPosNavigationEvent.BackFromCashPayment -> popBackStack() + is WooPosNavigationEvent.BackFromCashPayment -> popBackStack() + is WooPosNavigationEvent.OpenHomeFromCashPaymentAfterSuccessfulPayment -> + navigateToHomeScreenAfterSuccessfulCashPayment() } } diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 89816d08cc3..035956fd400 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -4314,6 +4314,7 @@ Error loading variations Cash payment + Complete order Customer diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentRepositoryTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentRepositoryTest.kt new file mode 100644 index 00000000000..8f47c9c71b0 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentRepositoryTest.kt @@ -0,0 +1,201 @@ +package com.woocommerce.android.ui.woopos.cashpayment + +import com.woocommerce.android.model.Order +import com.woocommerce.android.model.OrderMapper +import com.woocommerce.android.tools.SelectedSite +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.OrderEntity +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.WCOrderStatusModel +import org.wordpress.android.fluxc.model.gateways.WCGatewayModel +import org.wordpress.android.fluxc.store.WCGatewayStore +import org.wordpress.android.fluxc.store.WCOrderStore +import org.wordpress.android.fluxc.store.WCOrderStore.OnOrderChanged +import org.wordpress.android.fluxc.store.WCOrderStore.UpdateOrderResult + +class WooPosCashPaymentRepositoryTest { + + private val selectedSite: SelectedSite = mock() + private val orderStore: WCOrderStore = mock() + private val orderMapper: OrderMapper = mock() + private val gatewayStore: WCGatewayStore = mock() + + private lateinit var repository: WooPosCashPaymentRepository + + @Before + fun setUp() { + repository = WooPosCashPaymentRepository( + selectedSite, + orderStore, + orderMapper, + gatewayStore + ) + } + + @Test + fun `given valid orderId and site, when getOrderById, then return mapped order`() = runTest { + // GIVEN + val orderId = 123L + val site: SiteModel = mock() + val mockOrder: OrderEntity = mock() + val mappedOrder: Order = mock() + + whenever(selectedSite.get()).thenReturn(site) + whenever(orderStore.getOrderByIdAndSite(orderId, site)).thenReturn(mockOrder) + whenever(orderMapper.toAppModel(mockOrder)).thenReturn(mappedOrder) + + // WHEN + val result = repository.getOrderById(orderId) + + // THEN + assertThat(result).isEqualTo(mappedOrder) + verify(orderStore).getOrderByIdAndSite(orderId, site) + verify(orderMapper).toAppModel(mockOrder) + } + + @Test + fun `given invalid orderId, when getOrderById, then return null`() = runTest { + // GIVEN + val orderId = 456L + val site: SiteModel = mock() + + whenever(selectedSite.get()).thenReturn(site) + whenever(orderStore.getOrderByIdAndSite(orderId, site)).thenReturn(null) + + // WHEN + val result = repository.getOrderById(orderId) + + // THEN + assertThat(result).isNull() + verify(orderStore).getOrderByIdAndSite(orderId, site) + } + + @Test + fun `given valid orderId, when completeOrder, then return success`() = runTest { + // GIVEN + val orderId = 123L + val site: SiteModel = mock() + val gatewayTitle = "Pay in Person" + val codGateway: WCGatewayModel = mock { on { title }.thenReturn(gatewayTitle) } + val statusModel = WCOrderStatusModel(statusKey = Order.Status.Completed.value) + val updateResult = UpdateOrderResult.RemoteUpdateResult(mock { on { isError }.thenReturn(false) }) + + whenever(selectedSite.get()).thenReturn(site) + whenever(gatewayStore.getGateway(site, "cod")).thenReturn(codGateway) + whenever(orderStore.getOrderStatusForSiteAndKey(site, Order.Status.Completed.value)).thenReturn(statusModel) + whenever( + orderStore.updateOrderStatusAndPaymentMethod( + orderId = orderId, + site = site, + newStatus = statusModel, + newPaymentMethodId = "cod", + newPaymentMethodTitle = gatewayTitle + ) + ).thenReturn(flowOf(updateResult)) + + // WHEN + val result = repository.completeOrder(orderId) + + // THEN + assertThat(result.isSuccess).isTrue() + verify(orderStore).updateOrderStatusAndPaymentMethod( + orderId = orderId, + site = site, + newStatus = statusModel, + newPaymentMethodId = "cod", + newPaymentMethodTitle = gatewayTitle + ) + } + + @Test + fun `given valid orderId, when completeOrder, then return failure`() = runTest { + // GIVEN + val orderId = 123L + val site: SiteModel = mock() + val gatewayTitle = "Pay in Person" + val codGateway: WCGatewayModel = mock { on { title }.thenReturn(gatewayTitle) } + val statusModel = WCOrderStatusModel(statusKey = Order.Status.Completed.value) + val errorMessage = "Order update failed" + val updateResult = UpdateOrderResult.RemoteUpdateResult( + event = OnOrderChanged( + orderError = WCOrderStore.OrderError( + message = errorMessage + ) + ) + ) + + whenever(selectedSite.get()).thenReturn(site) + whenever(gatewayStore.getGateway(site, "cod")).thenReturn(codGateway) + whenever(orderStore.getOrderStatusForSiteAndKey(site, Order.Status.Completed.value)).thenReturn(statusModel) + whenever( + orderStore.updateOrderStatusAndPaymentMethod( + orderId = orderId, + site = site, + newStatus = statusModel, + newPaymentMethodId = "cod", + newPaymentMethodTitle = gatewayTitle + ) + ).thenReturn(flowOf(updateResult)) + + // WHEN + val result = repository.completeOrder(orderId) + + // THEN + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()?.message).isEqualTo(errorMessage) + verify(orderStore).updateOrderStatusAndPaymentMethod( + orderId = orderId, + site = site, + newStatus = statusModel, + newPaymentMethodId = "cod", + newPaymentMethodTitle = gatewayTitle + ) + } + + @Test + fun `given no status model, when completeOrder, then default status is used`() = runTest { + // GIVEN + val orderId = 789L + val site: SiteModel = mock() + val gatewayTitle = "Pay in Person" + val codGateway: WCGatewayModel = mock { on { title }.thenReturn(gatewayTitle) } + val updateResult = UpdateOrderResult.RemoteUpdateResult(mock { on { isError }.thenReturn(false) }) + + whenever(selectedSite.get()).thenReturn(site) + whenever(gatewayStore.getGateway(site, "cod")).thenReturn(codGateway) + whenever(orderStore.getOrderStatusForSiteAndKey(site, Order.Status.Completed.value)).thenReturn(null) + whenever( + orderStore.updateOrderStatusAndPaymentMethod( + orderId = orderId, + site = site, + newStatus = WCOrderStatusModel(statusKey = Order.Status.Completed.value).apply { + label = Order.Status.Completed.value + }, + newPaymentMethodId = "cod", + newPaymentMethodTitle = gatewayTitle + ) + ).thenReturn(flowOf(updateResult)) + + // WHEN + val result = repository.completeOrder(orderId) + + // THEN + assertThat(result.isSuccess).isTrue() + verify(orderStore).updateOrderStatusAndPaymentMethod( + orderId = orderId, + site = site, + newStatus = WCOrderStatusModel(statusKey = Order.Status.Completed.value).apply { + label = Order.Status.Completed.value + }, + newPaymentMethodId = "cod", + newPaymentMethodTitle = gatewayTitle + ) + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt index cb51974f395..d0098884a90 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt @@ -10,6 +10,7 @@ import com.woocommerce.android.ui.woopos.featureflags.WooPosIsCashPaymentsEnable import com.woocommerce.android.ui.woopos.featureflags.WooPosIsReceiptsEnabled import com.woocommerce.android.ui.woopos.home.ChildToParentEvent import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent +import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent.OrderSuccessfullyPaid import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel @@ -514,7 +515,8 @@ class WooPosTotalsViewModelTest { } @Test - fun `given payment status is success, when payment flow started, then OrderSuccessfullyPaid event and update state to PaymentSuccess`() = + @Suppress("LongMethod") + fun `given payment status is success, when payment flow started, then OrderSuccessfullyPaid event and PaymentSuccess`() = runTest { // GIVEN whenever(networkStatus.isConnected()).thenReturn(true) @@ -523,7 +525,9 @@ class WooPosTotalsViewModelTest { id = 1L ) ) - val parentToChildrenEventFlow = MutableStateFlow(ParentToChildrenEvent.CheckoutClicked(itemClickedData)) + val parentToChildrenEventFlow = MutableStateFlow( + ParentToChildrenEvent.CheckoutClicked(itemClickedData) + ) val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock { on { events }.thenReturn(parentToChildrenEventFlow) } @@ -553,7 +557,7 @@ class WooPosTotalsViewModelTest { } val paymentStatusFlow = - MutableStateFlow(WooPosCardReaderPaymentStatus.Unknown) + MutableStateFlow(WooPosCardReaderPaymentStatus.Success) whenever(cardReaderFacade.paymentStatus).thenReturn(paymentStatusFlow) val resourceProvider: ResourceProvider = mock { @@ -572,8 +576,7 @@ class WooPosTotalsViewModelTest { // WHEN viewModel.onUIEvent(WooPosTotalsUIEvent.CollectPaymentClicked) - paymentStatusFlow.value = WooPosCardReaderPaymentStatus.Success - advanceUntilIdle() + parentToChildrenEventFlow.value = OrderSuccessfullyPaid // THEN val state = viewModel.state.value