diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesFragment.kt index 678439da2007..afcdfe5ea689 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesFragment.kt @@ -17,6 +17,9 @@ import com.woocommerce.android.extensions.navigateBackWithNotice import com.woocommerce.android.extensions.navigateSafely import com.woocommerce.android.model.Order import com.woocommerce.android.ui.base.BaseFragment +import com.woocommerce.android.ui.filters.FilterHistoryType +import com.woocommerce.android.ui.filters.FilterHistoryViewModel +import com.woocommerce.android.ui.filters.SavedFilter import com.woocommerce.android.ui.main.AppBarStatus import com.woocommerce.android.ui.main.MainActivity.Companion.BackPressListener import com.woocommerce.android.ui.orders.creation.customerlist.OrderCustomerListFragment.Companion.KEY_CUSTOMER_RESULT @@ -25,6 +28,7 @@ import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.CU import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.PRODUCT import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryUiModel import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.OnShowOrders +import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.OpenFilterHistory import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.ShowFilterOptionsForCategory import com.woocommerce.android.ui.orders.list.OrderListFragment import com.woocommerce.android.ui.products.selector.ProductSelectorFragment @@ -78,6 +82,7 @@ class OrderFilterCategoriesFragment : override fun onCreateMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.menu_clear, menu) + inflater.inflate(R.menu.menu_filter_history, menu) } private fun handleResults() { @@ -96,10 +101,15 @@ class OrderFilterCategoriesFragment : handleResult(KEY_CUSTOMER_RESULT) { viewModel.onCustomerSelected(it) } + + handleResult(FilterHistoryViewModel.FILTER_HISTORY_RESULT_KEY) { + viewModel.onPastFilterSelected(it) + } } override fun onPrepareMenu(menu: Menu) { updateClearButtonVisibility(menu.findItem(R.id.menu_clear)) + menu.findItem(R.id.menu_filter_history).isVisible = viewModel.isFilterHistoryEnabled } override fun onMenuItemSelected(item: MenuItem): Boolean { @@ -110,6 +120,11 @@ class OrderFilterCategoriesFragment : true } + R.id.menu_filter_history -> { + viewModel.onFilterHistoryButtonClicked() + true + } + else -> false } } @@ -160,6 +175,13 @@ class OrderFilterCategoriesFragment : findNavController().navigateSafely(action) } + private fun navigateToFilterHistory() { + findNavController().navigateSafely( + OrderFilterCategoriesFragmentDirections + .actionOrderFilterCategoriesFragmentToFilterHistoryFragment(FilterHistoryType.ORDERS) + ) + } + private fun setUpObservers(viewModel: OrderFilterCategoriesViewModel) { viewModel.categories.observe(viewLifecycleOwner) { _, newValue -> showOrderFilters(newValue.list) @@ -175,6 +197,7 @@ class OrderFilterCategoriesFragment : OrderListFragment.FILTER_CHANGE_NOTICE_KEY ) + is OpenFilterHistory -> navigateToFilterHistory() is Exit -> findNavController().navigateUp() else -> event.isHandled = false } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesViewModel.kt index 58e1755192a5..95b0b1b1bd8c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesViewModel.kt @@ -9,6 +9,7 @@ import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.analytics.AnalyticsTrackerWrapper import com.woocommerce.android.model.Order import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.filters.SavedFilter import com.woocommerce.android.ui.orders.filters.data.OrderFiltersRepository import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.CUSTOMER @@ -21,9 +22,11 @@ import com.woocommerce.android.ui.orders.filters.domain.GetDateRangeFilterOption import com.woocommerce.android.ui.orders.filters.domain.GetOrderStatusFilterOptions import com.woocommerce.android.ui.orders.filters.domain.GetTrackingForFilterSelection import com.woocommerce.android.ui.orders.filters.domain.IsSalesChannelFilterSupported +import com.woocommerce.android.ui.orders.filters.domain.SaveOrderFilterToHistory import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryListViewState import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryUiModel import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.OnShowOrders +import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.OpenFilterHistory import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.ShowFilterOptionsForCategory import com.woocommerce.android.ui.orders.filters.model.OrderFilterOptionUiModel import com.woocommerce.android.ui.orders.filters.model.addFilterOptionAll @@ -34,6 +37,8 @@ import com.woocommerce.android.ui.orders.filters.model.markOptionAllIfNothingSel import com.woocommerce.android.ui.orders.filters.model.toOrderFilterOptionUiModel import com.woocommerce.android.ui.products.list.ProductListRepository import com.woocommerce.android.util.DateUtils +import com.woocommerce.android.util.FeatureFlag +import com.woocommerce.android.util.FeatureFlagRepository import com.woocommerce.android.viewmodel.LiveDataDelegate import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ShowDialog @@ -58,12 +63,17 @@ class OrderFilterCategoriesViewModel @Inject constructor( private val productRepository: ProductListRepository, private val customerStore: WCCustomerStore, private val selectedSite: SelectedSite, - private val isSalesChannelFilterSupported: IsSalesChannelFilterSupported + private val isSalesChannelFilterSupported: IsSalesChannelFilterSupported, + private val saveOrderFilterToHistory: SaveOrderFilterToHistory, + private val orderFilterHistoryMapper: OrderFilterHistoryMapper, + featureFlagRepository: FeatureFlagRepository ) : ScopedViewModel(savedState) { companion object { const val OLD_FILTER_SELECTION_KEY = "old_filter_selection_key" } + val isFilterHistoryEnabled: Boolean = featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY) + private var oldFilterSelection: List = savedState[OLD_FILTER_SELECTION_KEY] ?: emptyList() @@ -100,9 +110,29 @@ class OrderFilterCategoriesViewModel @Inject constructor( fun onShowOrdersClicked() { saveFiltersSelection(_categories.list) trackFilterSelection() + saveOrderFilterToHistory() triggerEvent(OnShowOrders) } + fun onFilterHistoryButtonClicked() { + analyticsTraWrapper.track( + AnalyticsEvent.FILTER_HISTORY_BUTTON_TAPPED, + mapOf(AnalyticsTracker.KEY_SOURCE to AnalyticsTracker.VALUE_FILTER_HISTORY_SOURCE_ORDERS) + ) + triggerEvent(OpenFilterHistory) + } + + fun onPastFilterSelected(savedFilter: SavedFilter) { + val historyData = orderFilterHistoryMapper.fromPayload(savedFilter.payload) ?: return + launch { + OrderListFilterCategory.entries.forEach { category -> + orderFilterRepository.setSelectedFilters(category, historyData.selections[category.name] ?: emptyList()) + } + orderFilterRepository.setCustomDateRange(historyData.customDateRangeStart, historyData.customDateRangeEnd) + _categories = OrderFilterCategories(buildFilterListUiModel()) + } + } + fun onClearFilters() { analyticsTraWrapper.track(AnalyticsEvent.ORDER_FILTER_LIST_CLEAR_MENU_BUTTON_TAPPED) _categories = OrderFilterCategories( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterHistoryMapper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterHistoryMapper.kt new file mode 100644 index 000000000000..441952f2a299 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterHistoryMapper.kt @@ -0,0 +1,52 @@ +package com.woocommerce.android.ui.orders.filters + +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryUiModel +import com.woocommerce.android.ui.orders.filters.model.OrderFilterOptionUiModel.Companion.DEFAULT_ALL_KEY +import javax.inject.Inject + +/** + * Encodes/decodes the order filter selection to and from the opaque `payload` string persisted in the + * filter history table. + * + * The payload is a canonical JSON serialization of the active selection (categories and option keys + * sorted) so that logically-identical selections dedup reliably. Decoding tolerates missing/unknown + * fields, and applying a decoded selection silently drops option keys that no longer exist because the + * filter screen only marks options selected when their key matches an available option. + */ +class OrderFilterHistoryMapper @Inject constructor( + private val gson: Gson +) { + fun toPayload( + categories: List, + customDateRangeStart: Long, + customDateRangeEnd: Long + ): String { + val selections = categories + .associate { category -> + category.categoryKey.name to category.orderFilterOptions + .filter { it.isSelected && it.key != DEFAULT_ALL_KEY } + .map { it.key } + .sorted() + } + .filterValues { it.isNotEmpty() } + .toSortedMap() + return gson.toJson( + OrderFilterHistoryData( + selections = selections, + customDateRangeStart = customDateRangeStart, + customDateRangeEnd = customDateRangeEnd + ) + ) + } + + fun fromPayload(payload: String): OrderFilterHistoryData? = + runCatching { gson.fromJson(payload, OrderFilterHistoryData::class.java) }.getOrNull() + + data class OrderFilterHistoryData( + @SerializedName("selections") val selections: Map> = emptyMap(), + @SerializedName("custom_date_range_start") val customDateRangeStart: Long = 0, + @SerializedName("custom_date_range_end") val customDateRangeEnd: Long = 0 + ) +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterOptionsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterOptionsViewModel.kt index 13e01ea45dbc..1447a0e5913b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterOptionsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterOptionsViewModel.kt @@ -16,6 +16,7 @@ import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.OR import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.PRODUCT import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.SALES_CHANNEL import com.woocommerce.android.ui.orders.filters.domain.GetTrackingForFilterSelection +import com.woocommerce.android.ui.orders.filters.domain.SaveOrderFilterToHistory import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.OnFilterOptionsSelectionUpdated import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.OnShowOrders import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.ShowCustomDateRangePicker @@ -40,6 +41,7 @@ class OrderFilterOptionsViewModel @Inject constructor( private val resourceProvider: ResourceProvider, private val orderFilterRepository: OrderFiltersRepository, private val getTrackingForFilterSelection: GetTrackingForFilterSelection, + private val saveOrderFilterToHistory: SaveOrderFilterToHistory, private val dateUtils: DateUtils ) : ScopedViewModel(savedState) { private val arguments: OrderFilterOptionsFragmentArgs by savedState.navArgs() @@ -73,6 +75,7 @@ class OrderFilterOptionsViewModel @Inject constructor( fun onShowOrdersClicked() { saveFiltersSelection() trackFilterSelection() + saveOrderFilterToHistory() triggerEvent(OnShowOrders) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/domain/SaveOrderFilterToHistory.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/domain/SaveOrderFilterToHistory.kt new file mode 100644 index 000000000000..143f6268b2b2 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/domain/SaveOrderFilterToHistory.kt @@ -0,0 +1,164 @@ +package com.woocommerce.android.ui.orders.filters.domain + +import com.woocommerce.android.R +import com.woocommerce.android.di.AppCoroutineScope +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.filters.FilterHistoryRepository +import com.woocommerce.android.ui.filters.FilterHistoryType +import com.woocommerce.android.ui.orders.filters.OrderFilterHistoryMapper +import com.woocommerce.android.ui.orders.filters.data.OrderFiltersRepository +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.CUSTOMER +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.DATE_RANGE +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.ORDER_STATUS +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.PRODUCT +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.SALES_CHANNEL +import com.woocommerce.android.ui.orders.filters.data.SalesChannel +import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryUiModel +import com.woocommerce.android.ui.orders.filters.model.OrderFilterOptionUiModel +import com.woocommerce.android.ui.orders.filters.model.OrderFilterOptionUiModel.Companion.DEFAULT_ALL_KEY +import com.woocommerce.android.ui.orders.filters.model.toOrderFilterOptionUiModel +import com.woocommerce.android.ui.products.list.ProductListRepository +import com.woocommerce.android.util.DateUtils +import com.woocommerce.android.util.FeatureFlag +import com.woocommerce.android.util.FeatureFlagRepository +import com.woocommerce.android.util.WooLog +import com.woocommerce.android.viewmodel.ResourceProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.wordpress.android.fluxc.store.WCCustomerStore +import javax.inject.Inject + +/** + * Persists the current order filter selection to the filter history. + * + * The current selection lives in [OrderFiltersRepository] (the source of truth once either the + * categories screen or an options sub-screen has saved it), so this reads from there rather than from + * a specific ViewModel's in-memory state. That's why it can be invoked from both "Show Orders" + * entry points and always capture the complete selection. + */ +class SaveOrderFilterToHistory @Inject constructor( + private val featureFlagRepository: FeatureFlagRepository, + private val orderFiltersRepository: OrderFiltersRepository, + private val getOrderStatusFilterOptions: GetOrderStatusFilterOptions, + private val getDateRangeFilterOptions: GetDateRangeFilterOptions, + private val dateUtils: DateUtils, + private val resourceProvider: ResourceProvider, + private val productListRepository: ProductListRepository, + private val customerStore: WCCustomerStore, + private val selectedSite: SelectedSite, + private val filterHistoryRepository: FilterHistoryRepository, + private val orderFilterHistoryMapper: OrderFilterHistoryMapper, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope +) { + /** + * Fire-and-forget: the save runs on the application scope so it survives the filter screen being + * dismissed, and callers can navigate away immediately without awaiting the DB write. Failures are + * logged rather than propagated — persisting history is best-effort and must never crash the app. + */ + operator fun invoke() { + appCoroutineScope.launch { + runCatching { save() } + .onFailure { WooLog.e(WooLog.T.ORDERS, "Failed to save order filter to history", it) } + } + } + + private suspend fun save() { + if (!featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)) return + val selectedCategories = buildSelectedCategories() + val hasSelection = selectedCategories.any { category -> + category.orderFilterOptions.any { it.isSelected && it.key != DEFAULT_ALL_KEY } + } + if (!hasSelection) return + // Store the range in epoch days, matching the unit setCustomDateRange expects on restore + // (getCustomDateRangeFilter returns millis, which would corrupt the days-based pref). + val customDateRange = orderFiltersRepository.getCustomDateRangeDays() + filterHistoryRepository.save( + type = FilterHistoryType.ORDERS, + payload = orderFilterHistoryMapper.toPayload( + categories = selectedCategories, + customDateRangeStart = customDateRange.first, + customDateRangeEnd = customDateRange.second + ), + readableString = buildReadableString(selectedCategories) + ) + } + + // Joins the selected option labels (excluding the "All" placeholder) into the label shown in the + // history list. Prefers displayValue when present so a custom date range shows its dates rather than + // the static "Custom Range" label; other options have no displayValue and fall back to displayName. + private fun buildReadableString(categories: List): String = + categories + .flatMap { it.orderFilterOptions } + .filter { it.isSelected && it.key != DEFAULT_ALL_KEY } + .joinToString( + separator = ", " + ) { it.displayValue?.takeIf { value -> value.isNotBlank() } ?: it.displayName } + + private suspend fun buildSelectedCategories(): List = listOf( + selectedCategory( + ORDER_STATUS, + // Use the bare status label (not toOrderFilterOptionUiModel, which appends the status count) + // so the saved history label reads e.g. "Processing" rather than "Processing (12)". + getOrderStatusFilterOptions().map { + OrderFilterOptionUiModel(key = it.key, displayName = it.label, isSelected = it.isSelected) + } + ), + selectedCategory( + DATE_RANGE, + getDateRangeFilterOptions().map { it.toOrderFilterOptionUiModel(resourceProvider, dateUtils) } + ), + selectedCategory(PRODUCT, productFilterOptions()), + selectedCategory(CUSTOMER, customerFilterOptions()), + selectedCategory(SALES_CHANNEL, salesChannelFilterOptions()) + ) + + private fun selectedCategory( + categoryKey: OrderListFilterCategory, + options: List + ) = OrderFilterCategoryUiModel( + categoryKey = categoryKey, + displayName = "", + displayValue = "", + orderFilterOptions = options.filter { it.isSelected } + ) + + private fun productFilterOptions(): List = listOfNotNull( + orderFiltersRepository.productFilter?.let { productId -> + OrderFilterOptionUiModel( + key = productId.toString(), + displayName = productListRepository.getProduct(productId)?.name ?: fallbackDisplayValue(productId), + isSelected = true + ) + } + ) + + private suspend fun customerFilterOptions(): List = listOfNotNull( + orderFiltersRepository.customerFilter?.let { customerId -> + val name = customerStore.getCustomerByRemoteId(selectedSite.get(), customerId) + ?.let { customer -> + (customer.firstName + " " + customer.lastName) + .ifBlank { customer.email } + .ifBlank { customer.username } + } ?: fallbackDisplayValue(customerId) + OrderFilterOptionUiModel(key = customerId.toString(), displayName = name, isSelected = true) + } + ) + + private fun salesChannelFilterOptions(): List = + orderFiltersRepository.getCurrentFilterSelection(SALES_CHANNEL).mapNotNull { key -> + val displayName = salesChannelDisplayName(key) ?: return@mapNotNull null + OrderFilterOptionUiModel(key = key, displayName = displayName, isSelected = true) + } + + private fun salesChannelDisplayName(key: String): String? = when (key) { + SalesChannel.POS.key -> resourceProvider.getString(R.string.point_of_sale) + SalesChannel.WEB_CHECKOUT.key -> + resourceProvider.getString(R.string.orderfilters_sales_channel_filter_web_checkout) + SalesChannel.WP_ADMIN.key -> resourceProvider.getString(R.string.orderfilters_sales_channel_filter_wp_admin) + else -> null + } + + private fun fallbackDisplayValue(id: Long): String = + resourceProvider.getString(R.string.orderfilters_selected_filter_fallback_display_value, id) +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/model/OrderFilterUiModels.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/model/OrderFilterUiModels.kt index 92790643ca6f..be18363620b8 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/model/OrderFilterUiModels.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/filters/model/OrderFilterUiModels.kt @@ -20,6 +20,8 @@ sealed class OrderFilterEvent : MultiLiveEvent.Event() { ) : OrderFilterEvent() object OnShowOrders : OrderFilterEvent() + + object OpenFilterHistory : OrderFilterEvent() } @Parcelize diff --git a/WooCommerce/src/main/res/menu/menu_filter_history.xml b/WooCommerce/src/main/res/menu/menu_filter_history.xml new file mode 100644 index 000000000000..1fea98aa7967 --- /dev/null +++ b/WooCommerce/src/main/res/menu/menu_filter_history.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesViewModelTest.kt index 3787409f7306..130d26467b08 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterCategoriesViewModelTest.kt @@ -3,8 +3,10 @@ package com.woocommerce.android.ui.orders.filters import androidx.lifecycle.SavedStateHandle import com.woocommerce.android.R import com.woocommerce.android.analytics.AnalyticsEvent +import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.analytics.AnalyticsTrackerWrapper import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.filters.SavedFilter import com.woocommerce.android.ui.orders.OrderTestUtils.generateOrderStatusOptions import com.woocommerce.android.ui.orders.filters.data.DateRange import com.woocommerce.android.ui.orders.filters.data.DateRangeFilterOption @@ -16,13 +18,16 @@ import com.woocommerce.android.ui.orders.filters.domain.GetDateRangeFilterOption import com.woocommerce.android.ui.orders.filters.domain.GetOrderStatusFilterOptions import com.woocommerce.android.ui.orders.filters.domain.GetTrackingForFilterSelection import com.woocommerce.android.ui.orders.filters.domain.IsSalesChannelFilterSupported +import com.woocommerce.android.ui.orders.filters.domain.SaveOrderFilterToHistory import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryListViewState import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryUiModel import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.OnShowOrders +import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.OpenFilterHistory import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.ShowFilterOptionsForCategory import com.woocommerce.android.ui.orders.filters.model.OrderFilterOptionUiModel import com.woocommerce.android.ui.products.list.ProductListRepository import com.woocommerce.android.util.DateUtils +import com.woocommerce.android.util.FeatureFlagRepository import com.woocommerce.android.util.getOrAwaitValue import com.woocommerce.android.viewmodel.BaseUnitTest import com.woocommerce.android.viewmodel.ResourceProvider @@ -32,10 +37,10 @@ import org.junit.Before import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.wordpress.android.fluxc.store.WCCustomerStore -import kotlin.test.assertTrue @ExperimentalCoroutinesApi class OrderFilterCategoriesViewModelTest : BaseUnitTest() { @@ -51,6 +56,9 @@ class OrderFilterCategoriesViewModelTest : BaseUnitTest() { private val customerStore: WCCustomerStore = mock() private val selectedSite: SelectedSite = mock() private val isSalesChannelFilterSupported: IsSalesChannelFilterSupported = mock() + private val saveOrderFilterToHistory: SaveOrderFilterToHistory = mock() + private val orderFilterHistoryMapper: OrderFilterHistoryMapper = mock() + private val featureFlagRepository: FeatureFlagRepository = mock() private lateinit var viewModel: OrderFilterCategoriesViewModel @@ -115,7 +123,7 @@ class OrderFilterCategoriesViewModelTest : BaseUnitTest() { viewModel.onClearFilters() - assertTrue(allFilterOptionsAreUnselected()) + assertThat(allFilterOptionsAreUnselected()).isTrue() } @Test @@ -216,12 +224,92 @@ class OrderFilterCategoriesViewModelTest : BaseUnitTest() { assertThat(selectedOption?.key).isEqualTo(SalesChannel.WEB_CHECKOUT.key) } + @Test + fun `when show orders is clicked, then the current filter is saved to history`() = testBlocking { + givenAFilterOptionHasBeenSelected(AN_ORDER_STATUS_FILTER_CATEGORY_WITH_SELECTED_FILTER) + + viewModel.onShowOrdersClicked() + + verify(saveOrderFilterToHistory).invoke() + } + + @Test + fun `when filter history button clicked, then entry point is tracked and history is opened`() { + viewModel.onFilterHistoryButtonClicked() + + verify(analyticsTraWrapper).track( + AnalyticsEvent.FILTER_HISTORY_BUTTON_TAPPED, + mapOf(AnalyticsTracker.KEY_SOURCE to AnalyticsTracker.VALUE_FILTER_HISTORY_SOURCE_ORDERS) + ) + assertThat(viewModel.event.value).isEqualTo(OpenFilterHistory) + } + + @Test + fun `given a past filter, when selected, then its selection is applied to the repository`() = testBlocking { + whenever(orderFilterHistoryMapper.fromPayload(A_PAYLOAD)).thenReturn( + OrderFilterHistoryMapper.OrderFilterHistoryData( + selections = mapOf(OrderListFilterCategory.ORDER_STATUS.name to listOf(ANY_ORDER_STATUS_KEY)), + customDateRangeStart = 0, + customDateRangeEnd = 0 + ) + ) + + viewModel.onPastFilterSelected(SavedFilter(readableString = A_READABLE_STRING, payload = A_PAYLOAD)) + + verify(orderFilterRepository).setSelectedFilters( + OrderListFilterCategory.ORDER_STATUS, + listOf(ANY_ORDER_STATUS_KEY) + ) + verify(orderFilterRepository).setCustomDateRange(0, 0) + } + + @Test + fun `given a past filter, when selected, then categories absent from it are cleared`() = testBlocking { + whenever(orderFilterHistoryMapper.fromPayload(A_PAYLOAD)).thenReturn( + OrderFilterHistoryMapper.OrderFilterHistoryData( + selections = mapOf(OrderListFilterCategory.ORDER_STATUS.name to listOf(ANY_ORDER_STATUS_KEY)) + ) + ) + + viewModel.onPastFilterSelected(SavedFilter(readableString = A_READABLE_STRING, payload = A_PAYLOAD)) + + verify(orderFilterRepository).setSelectedFilters(OrderListFilterCategory.DATE_RANGE, emptyList()) + verify(orderFilterRepository).setSelectedFilters(OrderListFilterCategory.PRODUCT, emptyList()) + verify(orderFilterRepository).setSelectedFilters(OrderListFilterCategory.CUSTOMER, emptyList()) + verify(orderFilterRepository).setSelectedFilters(OrderListFilterCategory.SALES_CHANNEL, emptyList()) + } + + @Test + fun `given a past filter with a custom date range, when selected, then the range is restored`() = testBlocking { + whenever(orderFilterHistoryMapper.fromPayload(A_PAYLOAD)).thenReturn( + OrderFilterHistoryMapper.OrderFilterHistoryData( + selections = mapOf(OrderListFilterCategory.DATE_RANGE.name to listOf("custom_range")), + customDateRangeStart = 111, + customDateRangeEnd = 222 + ) + ) + + viewModel.onPastFilterSelected(SavedFilter(readableString = A_READABLE_STRING, payload = A_PAYLOAD)) + + verify(orderFilterRepository).setCustomDateRange(111, 222) + } + + @Test + fun `given an undecodable past filter, when selected, then the repository is not touched`() = testBlocking { + whenever(orderFilterHistoryMapper.fromPayload(A_PAYLOAD)).thenReturn(null) + + viewModel.onPastFilterSelected(SavedFilter(readableString = A_READABLE_STRING, payload = A_PAYLOAD)) + + verify(orderFilterRepository, never()).setSelectedFilters(any(), any()) + verify(orderFilterRepository, never()).setCustomDateRange(any(), any()) + } + private fun allFilterOptionsAreUnselected() = currentCategoryList .map { it.orderFilterOptions.any { filterOption -> filterOption.isSelected && filterOption.key != OrderFilterOptionUiModel.DEFAULT_ALL_KEY } - }.all { true } + }.none { it } private fun givenAFilterOptionHasBeenSelected(updatedFilters: OrderFilterCategoryUiModel) { viewModel.onFilterOptionsUpdated(updatedFilters) @@ -240,7 +328,10 @@ class OrderFilterCategoriesViewModelTest : BaseUnitTest() { productRepository = productListRepository, customerStore = customerStore, selectedSite = selectedSite, - isSalesChannelFilterSupported = isSalesChannelFilterSupported + isSalesChannelFilterSupported = isSalesChannelFilterSupported, + saveOrderFilterToHistory = saveOrderFilterToHistory, + orderFilterHistoryMapper = orderFilterHistoryMapper, + featureFlagRepository = featureFlagRepository ) } @@ -280,6 +371,8 @@ class OrderFilterCategoriesViewModelTest : BaseUnitTest() { private companion object { const val DEFAULT_FILTER_TITLE = "Title" const val ANY_ORDER_STATUS_KEY = "OrderStatusOptionKey" + const val A_PAYLOAD = "payload" + const val A_READABLE_STRING = "readable" val SELECTED_ORDER_STATUS_FILTER_OPTION = OrderFilterOptionUiModel( key = ANY_ORDER_STATUS_KEY, displayName = "OrderStatus", diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterHistoryMapperTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterHistoryMapperTest.kt new file mode 100644 index 000000000000..e0f65eff5ce9 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterHistoryMapperTest.kt @@ -0,0 +1,105 @@ +package com.woocommerce.android.ui.orders.filters + +import com.google.gson.Gson +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory +import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryUiModel +import com.woocommerce.android.ui.orders.filters.model.OrderFilterOptionUiModel +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class OrderFilterHistoryMapperTest { + private val sut = OrderFilterHistoryMapper(Gson()) + + @Test + fun `given a selection, when encoding then decoding, then the selection round-trips`() { + val categories = listOf( + category( + OrderListFilterCategory.ORDER_STATUS, + option("processing", "Processing", isSelected = true), + option("completed", "Completed", isSelected = true) + ) + ) + + val payload = sut.toPayload(categories, customDateRangeStart = 111, customDateRangeEnd = 222) + val decoded = sut.fromPayload(payload) + + assertThat(decoded?.selections).isEqualTo( + mapOf(OrderListFilterCategory.ORDER_STATUS.name to listOf("completed", "processing")) + ) + assertThat(decoded?.customDateRangeStart).isEqualTo(111) + assertThat(decoded?.customDateRangeEnd).isEqualTo(222) + } + + @Test + fun `given selections in different orders, when encoded, then payloads are identical (canonical)`() { + val a = listOf( + category( + OrderListFilterCategory.ORDER_STATUS, + option("processing", "Processing", isSelected = true), + option("completed", "Completed", isSelected = true) + ) + ) + val b = listOf( + category( + OrderListFilterCategory.ORDER_STATUS, + option("completed", "Completed", isSelected = true), + option("processing", "Processing", isSelected = true) + ) + ) + + assertThat(sut.toPayload(a, 0, 0)).isEqualTo(sut.toPayload(b, 0, 0)) + } + + @Test + fun `given an invalid payload, when decoding, then null is returned`() { + assertThat(sut.fromPayload("not-json")).isNull() + } + + @Test + fun `given an empty json payload, when decoding, then defaults are returned`() { + val decoded = sut.fromPayload("{}") + + assertThat(decoded).isNotNull + assertThat(decoded?.selections).isEmpty() + assertThat(decoded?.customDateRangeStart).isEqualTo(0) + assertThat(decoded?.customDateRangeEnd).isEqualTo(0) + } + + @Test + fun `given a selected All option, when encoding, then it is excluded from the payload`() { + val categories = listOf( + category( + OrderListFilterCategory.ORDER_STATUS, + option(OrderFilterOptionUiModel.DEFAULT_ALL_KEY, "All", isSelected = true), + option("processing", "Processing", isSelected = true) + ) + ) + + assertThat(sut.fromPayload(sut.toPayload(categories, 0, 0))?.selections).isEqualTo( + mapOf(OrderListFilterCategory.ORDER_STATUS.name to listOf("processing")) + ) + } + + @Test + fun `given a category whose only selection is All, when encoding, then the category is absent from the payload`() { + val categories = listOf( + category( + OrderListFilterCategory.ORDER_STATUS, + option(OrderFilterOptionUiModel.DEFAULT_ALL_KEY, "All", isSelected = true) + ) + ) + + assertThat(sut.fromPayload(sut.toPayload(categories, 0, 0))?.selections).isEmpty() + } + + private fun category(key: OrderListFilterCategory, vararg options: OrderFilterOptionUiModel) = + OrderFilterCategoryUiModel( + categoryKey = key, + displayName = key.name, + displayValue = "", + orderFilterOptions = options.toList() + ) + + private fun option(key: String, displayName: String, isSelected: Boolean) = + OrderFilterOptionUiModel(key = key, displayName = displayName, isSelected = isSelected) +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterOptionsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterOptionsViewModelTest.kt new file mode 100644 index 000000000000..4d3919710477 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/OrderFilterOptionsViewModelTest.kt @@ -0,0 +1,77 @@ +package com.woocommerce.android.ui.orders.filters + +import com.woocommerce.android.ui.orders.filters.data.OrderFiltersRepository +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory +import com.woocommerce.android.ui.orders.filters.domain.GetTrackingForFilterSelection +import com.woocommerce.android.ui.orders.filters.domain.SaveOrderFilterToHistory +import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryUiModel +import com.woocommerce.android.ui.orders.filters.model.OrderFilterEvent.OnShowOrders +import com.woocommerce.android.ui.orders.filters.model.OrderFilterOptionUiModel +import com.woocommerce.android.util.DateUtils +import com.woocommerce.android.viewmodel.BaseUnitTest +import com.woocommerce.android.viewmodel.ResourceProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@OptIn(ExperimentalCoroutinesApi::class) +class OrderFilterOptionsViewModelTest : BaseUnitTest() { + private val resourceProvider: ResourceProvider = mock { + on { getString(any()) } doReturn "AnyString" + } + private val orderFilterRepository: OrderFiltersRepository = mock() + private val getTrackingForFilterSelection: GetTrackingForFilterSelection = mock() + private val saveOrderFilterToHistory: SaveOrderFilterToHistory = mock() + private val dateUtils: DateUtils = mock() + + private fun createViewModel() = OrderFilterOptionsViewModel( + savedState = OrderFilterOptionsFragmentArgs(filterCategory = A_CATEGORY).toSavedStateHandle(), + resourceProvider = resourceProvider, + orderFilterRepository = orderFilterRepository, + getTrackingForFilterSelection = getTrackingForFilterSelection, + saveOrderFilterToHistory = saveOrderFilterToHistory, + dateUtils = dateUtils + ) + + @Test + fun `when show orders is clicked, then the current filter is saved to history`() = testBlocking { + val viewModel = createViewModel() + + viewModel.onShowOrdersClicked() + + verify(saveOrderFilterToHistory).invoke() + } + + @Test + fun `when show orders is clicked, then OnShowOrders event is triggered`() = testBlocking { + val viewModel = createViewModel() + + viewModel.onShowOrdersClicked() + + assertThat(viewModel.event.value).isEqualTo(OnShowOrders) + } + + @Test + fun `when show orders is clicked, then the selection is persisted`() = testBlocking { + val viewModel = createViewModel() + + viewModel.onShowOrdersClicked() + + verify(orderFilterRepository).setSelectedFilters(OrderListFilterCategory.ORDER_STATUS, listOf("processing")) + } + + private companion object { + val A_CATEGORY = OrderFilterCategoryUiModel( + categoryKey = OrderListFilterCategory.ORDER_STATUS, + displayName = "", + displayValue = "", + orderFilterOptions = listOf( + OrderFilterOptionUiModel(key = "processing", displayName = "Processing", isSelected = true) + ) + ) + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/domain/SaveOrderFilterToHistoryTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/domain/SaveOrderFilterToHistoryTest.kt new file mode 100644 index 000000000000..1b5e4d17a8c6 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/filters/domain/SaveOrderFilterToHistoryTest.kt @@ -0,0 +1,210 @@ +package com.woocommerce.android.ui.orders.filters.domain + +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.filters.FilterHistoryRepository +import com.woocommerce.android.ui.filters.FilterHistoryType +import com.woocommerce.android.ui.orders.filters.OrderFilterHistoryMapper +import com.woocommerce.android.ui.orders.filters.data.OrderFiltersRepository +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.CUSTOMER +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.PRODUCT +import com.woocommerce.android.ui.orders.filters.data.OrderListFilterCategory.SALES_CHANNEL +import com.woocommerce.android.ui.orders.filters.data.OrderStatusOption +import com.woocommerce.android.ui.orders.filters.data.SalesChannel +import com.woocommerce.android.ui.orders.filters.model.OrderFilterCategoryUiModel +import com.woocommerce.android.ui.products.list.ProductListRepository +import com.woocommerce.android.util.DateUtils +import com.woocommerce.android.util.FeatureFlag +import com.woocommerce.android.util.FeatureFlagRepository +import com.woocommerce.android.viewmodel.BaseUnitTest +import com.woocommerce.android.viewmodel.ResourceProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.WCProductModel +import org.wordpress.android.fluxc.model.customer.WCCustomerModel +import org.wordpress.android.fluxc.store.WCCustomerStore + +@OptIn(ExperimentalCoroutinesApi::class) +class SaveOrderFilterToHistoryTest : BaseUnitTest() { + private val featureFlagRepository: FeatureFlagRepository = mock() + private val orderFiltersRepository: OrderFiltersRepository = mock() + private val getOrderStatusFilterOptions: GetOrderStatusFilterOptions = mock() + private val getDateRangeFilterOptions: GetDateRangeFilterOptions = mock() + private val dateUtils: DateUtils = mock() + private val resourceProvider: ResourceProvider = mock() + private val productListRepository: ProductListRepository = mock() + private val customerStore: WCCustomerStore = mock() + private val selectedSite: SelectedSite = mock() + private val filterHistoryRepository: FilterHistoryRepository = mock() + private val orderFilterHistoryMapper: OrderFilterHistoryMapper = mock() + + private val sut = SaveOrderFilterToHistory( + featureFlagRepository = featureFlagRepository, + orderFiltersRepository = orderFiltersRepository, + getOrderStatusFilterOptions = getOrderStatusFilterOptions, + getDateRangeFilterOptions = getDateRangeFilterOptions, + dateUtils = dateUtils, + resourceProvider = resourceProvider, + productListRepository = productListRepository, + customerStore = customerStore, + selectedSite = selectedSite, + filterHistoryRepository = filterHistoryRepository, + orderFilterHistoryMapper = orderFilterHistoryMapper, + appCoroutineScope = CoroutineScope(coroutinesTestRule.testDispatcher) + ) + + @Test + fun `given feature flag disabled, when invoked, then nothing is saved`() = testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(false) + + sut() + + verify(filterHistoryRepository, never()).save(any(), any(), any()) + } + + @Test + fun `given no filter is selected, when invoked, then nothing is saved`() = testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(true) + whenever(getOrderStatusFilterOptions.invoke()).thenReturn( + listOf(OrderStatusOption("processing", "Processing", statusCount = 0, isSelected = false)) + ) + whenever(getDateRangeFilterOptions.invoke()).thenReturn(emptyList()) + whenever(orderFiltersRepository.productFilter).thenReturn(null) + whenever(orderFiltersRepository.customerFilter).thenReturn(null) + whenever(orderFiltersRepository.getCurrentFilterSelection(OrderListFilterCategory.SALES_CHANNEL)) + .thenReturn(emptyList()) + + sut() + + verify(filterHistoryRepository, never()).save(any(), any(), any()) + } + + @Test + fun `given a filter is selected, when invoked, then it is saved to history`() = testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(true) + whenever(getOrderStatusFilterOptions.invoke()).thenReturn( + listOf(OrderStatusOption("processing", "Processing", statusCount = 0, isSelected = true)) + ) + whenever(getDateRangeFilterOptions.invoke()).thenReturn(emptyList()) + whenever(orderFiltersRepository.productFilter).thenReturn(null) + whenever(orderFiltersRepository.customerFilter).thenReturn(null) + whenever(orderFiltersRepository.getCurrentFilterSelection(OrderListFilterCategory.SALES_CHANNEL)) + .thenReturn(emptyList()) + whenever(orderFiltersRepository.getCustomDateRangeDays()).thenReturn(0L to 0L) + whenever(orderFilterHistoryMapper.toPayload(any(), any(), any())).thenReturn(A_PAYLOAD) + + sut() + + verify(filterHistoryRepository).save(FilterHistoryType.ORDERS, A_PAYLOAD, A_READABLE_STRING) + } + + @Test + fun `given a selected order status with a count, when invoked, then the label ignores the count`() = testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(true) + whenever(getOrderStatusFilterOptions.invoke()).thenReturn( + listOf(OrderStatusOption("processing", "Processing", statusCount = 12, isSelected = true)) + ) + whenever(getDateRangeFilterOptions.invoke()).thenReturn(emptyList()) + whenever(orderFiltersRepository.productFilter).thenReturn(null) + whenever(orderFiltersRepository.customerFilter).thenReturn(null) + whenever(orderFiltersRepository.getCurrentFilterSelection(OrderListFilterCategory.SALES_CHANNEL)) + .thenReturn(emptyList()) + whenever(orderFiltersRepository.getCustomDateRangeDays()).thenReturn(0L to 0L) + + sut() + + val readableCaptor = argumentCaptor() + verify(filterHistoryRepository).save(any(), anyOrNull(), readableCaptor.capture()) + assertThat(readableCaptor.firstValue).isEqualTo("Processing") + } + + @Test + fun `given a selected product, when invoked, then the product name is used as the label`() = testBlocking { + givenNothingSelected() + whenever(orderFiltersRepository.productFilter).thenReturn(PRODUCT_ID) + whenever(productListRepository.getProduct(PRODUCT_ID)).thenReturn(WCProductModel().copy(name = "Widget")) + + sut() + + val option = capturedCategory(PRODUCT).orderFilterOptions.first() + assertThat(option.key).isEqualTo(PRODUCT_ID.toString()) + assertThat(option.displayName).isEqualTo("Widget") + } + + @Test + fun `given a selected customer, when invoked, then the customer name is used as the label`() = testBlocking { + givenNothingSelected() + whenever(orderFiltersRepository.customerFilter).thenReturn(CUSTOMER_ID) + whenever(selectedSite.get()).thenReturn(SiteModel()) + whenever(customerStore.getCustomerByRemoteId(any(), eq(CUSTOMER_ID))) + .thenReturn(WCCustomerModel(firstName = "John", lastName = "Doe")) + + sut() + + val option = capturedCategory(CUSTOMER).orderFilterOptions.first() + assertThat(option.key).isEqualTo(CUSTOMER_ID.toString()) + assertThat(option.displayName).isEqualTo("John Doe") + } + + @Test + fun `given selected sales channels, when invoked, then known keys are mapped and unknown keys dropped`() = + testBlocking { + givenNothingSelected() + whenever(orderFiltersRepository.getCurrentFilterSelection(SALES_CHANNEL)) + .thenReturn(listOf(SalesChannel.POS.key, "unknown_channel")) + whenever(resourceProvider.getString(any())).thenReturn("Point of Sale") + + sut() + + val options = capturedCategory(SALES_CHANNEL).orderFilterOptions + assertThat(options.map { it.key }).containsExactly(SalesChannel.POS.key) + assertThat(options.first().displayName).isEqualTo("Point of Sale") + } + + @Test + fun `given a custom date range, when invoked, then its bounds are forwarded to the payload`() = testBlocking { + givenNothingSelected() + whenever(getOrderStatusFilterOptions.invoke()).thenReturn( + listOf(OrderStatusOption("processing", "Processing", statusCount = 0, isSelected = true)) + ) + whenever(orderFiltersRepository.getCustomDateRangeDays()).thenReturn(111L to 222L) + + sut() + + verify(orderFilterHistoryMapper).toPayload(any(), eq(111L), eq(222L)) + } + + private suspend fun givenNothingSelected() { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(true) + whenever(getOrderStatusFilterOptions.invoke()).thenReturn(emptyList()) + whenever(getDateRangeFilterOptions.invoke()).thenReturn(emptyList()) + whenever(orderFiltersRepository.productFilter).thenReturn(null) + whenever(orderFiltersRepository.customerFilter).thenReturn(null) + whenever(orderFiltersRepository.getCurrentFilterSelection(SALES_CHANNEL)).thenReturn(emptyList()) + whenever(orderFiltersRepository.getCustomDateRangeDays()).thenReturn(0L to 0L) + } + + private fun capturedCategory(categoryKey: OrderListFilterCategory): OrderFilterCategoryUiModel { + val categoriesCaptor = argumentCaptor>() + verify(orderFilterHistoryMapper).toPayload(categoriesCaptor.capture(), any(), any()) + return categoriesCaptor.firstValue.first { it.categoryKey == categoryKey } + } + + private companion object { + const val A_PAYLOAD = "payload" + const val A_READABLE_STRING = "Processing" + const val PRODUCT_ID = 123L + const val CUSTOMER_ID = 456L + } +}