Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Bulk Update Orders] Better handling for partial success and other results from updating #13275

Open
wants to merge 5 commits into
base: trunk
Choose a base branch
from

Conversation

hafizrahman
Copy link
Contributor

@hafizrahman hafizrahman commented Jan 9, 2025

Description

The API call for bulk order status update returns values for both successful and failed order updates, which means a call can either be successful, partially successful, or different form of failures.

Previously, the app simply treats any result as success as long as there's at least 1 successful Order update (and informs merchants such). Partial success/failure is logged in application log but not informed to user.

This PR improves the success handling by creating specific types based on the result, each having its own snackbar message:

Type Message to merchant
PartialSuccess "X order(s) updated, and Y order(s) failed to update. Please try again."
AllSuccess "Status updated!"
AllFailed "Failed to update orders. Please try again"
NoOrdersUpdated "No orders updated. Please try again."
Error "An error occured"

Steps to reproduce

I'd recommend using API Faker to test the different cases:

  1. Go to Settings > Developer Options > API Faker
  2. Add new endpoint:
    • Type: WordPress REST API
    • HTTP Method: POST
    • Path: /wc/v3/orders/batch
    • Query parameters: %
    • Response: Status code 200, then for the Body value use the example values shared below.

TC 1: Partial success:

  1. Copy response example from here and paste to API faker body, save it,
  2. Go to Orders List, long press to select and Order, then tap triple dot and Update Status,
  3. Update to any status (doesn't matter since we're overriding the response with API faker),
  4. Ensure the same message is shown as in the table above.

TC 2: Full success:

  1. Copy response example from here and paste to API faker body, save it,
  2. Go to Orders List, long press to select and Order, then tap triple dot and Update Status,
  3. Update to any status (doesn't matter since we're overriding the response with API faker),
  4. Ensure the same message is shown as in the table above.

TC 3: All failed:

  1. Copy response example from here and paste to API faker body, save it,
  2. Go to Orders List, long press to select and Order, then tap triple dot and Update Status,
  3. Update to any status (doesn't matter since we're overriding the response with API faker),
  4. Ensure the same message is shown as in the table above.

TC 4: No Orders Updated:

  1. Copy response example from here and paste to API faker body, save it,
  2. Go to Orders List, long press to select and Order, then tap triple dot and Update Status,
  3. Update to any status (doesn't matter since we're overriding the response with API faker),
  4. Ensure the same message is shown as in the table above.

TC 5: Error:

  1. Update API Faker to return 404 as status code, and save it.
  2. Go to Orders List, long press to select and Order, then tap triple dot and Update Status,
  3. Update to any status (doesn't matter since we're overriding the response with API faker),
  4. Ensure the same message is shown as in the table above.

Testing information

The tests that have been performed

I have tested all five TCs using API Faker as above.

Images/gif

n/a

  • I have considered if this change warrants release notes and have added them to RELEASE-NOTES.txt if necessary. Use the "[Internal]" label for non-user-facing changes.

Reviewer (or Author, in the case of optional code reviews):

Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement:

  • The PR is small and has a clear, single focus, or a valid explanation is provided in the description. If needed, please request to split it into smaller PRs.
  • Ensure Adequate Unit Test Coverage: The changes are reasonably covered by unit tests or an explanation is provided in the PR description.
  • Manual Testing: The author listed all the tests they ran, including smoke tests when needed (e.g., for refactorings). The reviewer confirmed that the PR works as expected on big (tablet) and small (phone) in case of UI changes, and no regressions are added.

@hafizrahman hafizrahman added this to the 21.4 milestone Jan 9, 2025
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Jan 9, 2025

📲 You can test the changes from this Pull Request in WooCommerce-Wear Android by scanning the QR code below to install the corresponding build.
App Name WooCommerce-Wear Android
Platform⌚️ Wear OS
FlavorJalapeno
Build TypeDebug
Commit1ab82d5
Direct Downloadwoocommerce-wear-prototype-build-pr13275-1ab82d5.apk

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Jan 9, 2025

📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.

App Name WooCommerce Android
Platform📱 Mobile
FlavorJalapeno
Build TypeDebug
Commit1ab82d5
Direct Downloadwoocommerce-prototype-build-pr13275-1ab82d5.apk

@codecov-commenter
Copy link

codecov-commenter commented Jan 9, 2025

Codecov Report

Attention: Patch coverage is 68.51852% with 17 lines in your changes missing coverage. Please review.

Project coverage is 40.81%. Comparing base (c2c1e55) to head (1ab82d5).
Report is 32 commits behind head on trunk.

Files with missing lines Patch % Lines
...erce/android/ui/orders/list/OrderListRepository.kt 0.00% 11 Missing ⚠️
...merce/android/ui/orders/list/OrderListViewModel.kt 82.85% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##              trunk   #13275      +/-   ##
============================================
+ Coverage     40.79%   40.81%   +0.01%     
- Complexity     6411     6420       +9     
============================================
  Files          1353     1354       +1     
  Lines         77678    77713      +35     
  Branches      10687    10696       +9     
============================================
+ Hits          31689    31718      +29     
- Misses        43182    43187       +5     
- Partials       2807     2808       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@hafizrahman hafizrahman modified the milestones: 21.4, 21.5 Jan 10, 2025
@hafizrahman hafizrahman added type: enhancement A request for an enhancement. feature: order list Related to the order list. labels Jan 10, 2025
}

@Test
fun `when bulk update fully succeeds, then refresh and exit selection mode`() = testBlocking {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ I'd love to be able to check Event and message here also, but the string and event happens separately when observing pagedListWrapper.data here https://github.com/woocommerce/woocommerce-android/pull/13275/files#diff-daf78e7103c7d0e51140c59b8490193f96db648214827d13e0d3f66c42c576d9R485 and I can't figure out exactly how to mock that. Suggestions welcome.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simulate this with something like this patch:

Index: WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt
--- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt	(revision 1ab82d50733c4e2425daa593dec1d60a5ef4aa67)
+++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt	(date 1736932809675)
@@ -1,6 +1,7 @@
 package com.woocommerce.android.ui.orders
 
 import androidx.lifecycle.MutableLiveData
+import androidx.paging.PagedList
 import com.google.android.material.snackbar.Snackbar
 import com.woocommerce.android.AppPrefsWrapper
 import com.woocommerce.android.FeedbackPrefs
@@ -1126,6 +1127,8 @@
         whenever(networkStatus.isConnected()).thenReturn(true)
         whenever(orderListRepository.bulkUpdateOrderStatus(any(), any()))
             .thenReturn(BulkUpdateOrderResult.AllSuccess)
+        val pagedListData = MutableLiveData<PagedList<OrderListItemUIType>>(mock())
+        whenever(pagedListWrapper.data).thenReturn(pagedListData)
 
         // First load order to initialize orderPagedListWrapper, then enter selection mode
         viewModel.loadOrders()
@@ -1134,9 +1137,14 @@
 
         // When
         viewModel.onBulkOrderStatusChanged(listOf(1L, 2L), Order.Status.Completed)
+        pagedListData.value = mock()
 
         // Then
         assertThat(viewModel.isSelecting()).isFalse()
+        val expectedEvent = OrderListEvent.ShowSnackbarString(
+            resourceProvider.getString(R.string.orderlist_bulk_update_status_updated)
+        )
+        assertThat(viewModel.event.value).isEqualTo(expectedEvent)
 
         // Invoked once during loadOrders() and once during onBulkOrderStatusChanged()
         verify(viewModel.ordersPagedListWrapper, times(2))?.fetchFirstPage()

We here force sending a different instance of PagedList by the second mock() call, which triggers the SnackBar.

@hafizrahman hafizrahman marked this pull request as ready for review January 10, 2025 14:05
@hichamboushaba hichamboushaba self-assigned this Jan 10, 2025
Copy link
Member

@hichamboushaba hichamboushaba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late review @hafizrahman, this slipped my mind.

Nice work here, it works well, I left some comments and tried to answer your question, but nothing blocker 👏

when (result) {
is BulkUpdateOrderResult.AllSuccess,
is BulkUpdateOrderResult.PartialSuccess -> {
isQueueingBulkUpdateSuccessMessage = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np, I understand why you are using an additional class variable for this, but personally I try to avoid them when it's possible, because they make the code more spread across different areas and hard to follow, and also they are not easy to maintain.
I have a suggestion on how to achieve what you want here without using them, you can check it by applying this patch:

Index: WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt	(revision 1ab82d50733c4e2425daa593dec1d60a5ef4aa67)
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt	(date 1736931926138)
@@ -11,6 +11,7 @@
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MediatorLiveData
 import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.asLiveData
 import androidx.lifecycle.viewModelScope
@@ -31,6 +32,7 @@
 import com.woocommerce.android.analytics.deviceTypeToAnalyticsString
 import com.woocommerce.android.extensions.NotificationReceivedEvent
 import com.woocommerce.android.extensions.WindowSizeClass
+import com.woocommerce.android.extensions.drop
 import com.woocommerce.android.extensions.filter
 import com.woocommerce.android.extensions.filterNotNull
 import com.woocommerce.android.extensions.runWithContext
@@ -229,9 +231,6 @@
 
     fun isSelecting() = viewState.orderListState == ViewState.OrderListState.Selecting
 
-    private var isQueueingBulkUpdateSuccessMessage = false
-    private var bulkUpdateSuccessMessage = ""
-
     init {
         lifecycleRegistry.currentState = Lifecycle.State.CREATED
         lifecycleRegistry.currentState = Lifecycle.State.STARTED
@@ -481,14 +480,6 @@
             _isLoadingMore.value = it
         }
 
-        // Observe status changes in the data
-        pagedListWrapper.data.distinct().observe(this) {
-            if (isQueueingBulkUpdateSuccessMessage) {
-                isQueueingBulkUpdateSuccessMessage = false
-                triggerEvent(OrderListEvent.ShowSnackbarString(bulkUpdateSuccessMessage))
-            }
-        }
-
         pagedListWrapper.listError
             .filter { !dismissListErrors }
             .filterNotNull()
@@ -1017,20 +1008,28 @@
         when (result) {
             is BulkUpdateOrderResult.AllSuccess,
             is BulkUpdateOrderResult.PartialSuccess -> {
-                isQueueingBulkUpdateSuccessMessage = true
-                bulkUpdateSuccessMessage = when (result) {
-                    is BulkUpdateOrderResult.AllSuccess -> resourceProvider.getString(
-                        R.string.orderlist_bulk_update_status_updated
-                    )
+                // Prepare to show a success message after the list has been updated
+                val observable = ordersPagedListWrapper?.data?.drop(1)
+                observable?.observe(this, object : Observer<PagedOrdersList> {
+                    override fun onChanged(value: PagedOrdersList) {
+                        val message = when (result) {
+                            is BulkUpdateOrderResult.AllSuccess -> resourceProvider.getString(
+                                R.string.orderlist_bulk_update_status_updated
+                            )
 
-                    is BulkUpdateOrderResult.PartialSuccess -> resourceProvider.getString(
-                        R.string.orderlist_bulk_update_result_partial_success,
-                        result.successCount,
-                        result.failureCount
-                    )
+                            is BulkUpdateOrderResult.PartialSuccess -> resourceProvider.getString(
+                                R.string.orderlist_bulk_update_result_partial_success,
+                                result.successCount,
+                                result.failureCount
+                            )
 
-                    else -> resourceProvider.getString(R.string.orderlist_bulk_update_status_updated)
-                }
+                            else -> resourceProvider.getString(R.string.orderlist_bulk_update_status_updated)
+                        }
+                        triggerEvent(OrderListEvent.ShowSnackbarString(message))
+                        observable.removeObserver(this)
+                    }
+                })
+
                 ordersPagedListWrapper?.fetchFirstPage()
                 trackBulkOrderUpdateSuccess()
             }

Applying this is not required, I just wanted to share it with you to see how we can add a pending job to be executed just once.

}

@Test
fun `when bulk update fully succeeds, then refresh and exit selection mode`() = testBlocking {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simulate this with something like this patch:

Index: WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt
--- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt	(revision 1ab82d50733c4e2425daa593dec1d60a5ef4aa67)
+++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt	(date 1736932809675)
@@ -1,6 +1,7 @@
 package com.woocommerce.android.ui.orders
 
 import androidx.lifecycle.MutableLiveData
+import androidx.paging.PagedList
 import com.google.android.material.snackbar.Snackbar
 import com.woocommerce.android.AppPrefsWrapper
 import com.woocommerce.android.FeedbackPrefs
@@ -1126,6 +1127,8 @@
         whenever(networkStatus.isConnected()).thenReturn(true)
         whenever(orderListRepository.bulkUpdateOrderStatus(any(), any()))
             .thenReturn(BulkUpdateOrderResult.AllSuccess)
+        val pagedListData = MutableLiveData<PagedList<OrderListItemUIType>>(mock())
+        whenever(pagedListWrapper.data).thenReturn(pagedListData)
 
         // First load order to initialize orderPagedListWrapper, then enter selection mode
         viewModel.loadOrders()
@@ -1134,9 +1137,14 @@
 
         // When
         viewModel.onBulkOrderStatusChanged(listOf(1L, 2L), Order.Status.Completed)
+        pagedListData.value = mock()
 
         // Then
         assertThat(viewModel.isSelecting()).isFalse()
+        val expectedEvent = OrderListEvent.ShowSnackbarString(
+            resourceProvider.getString(R.string.orderlist_bulk_update_status_updated)
+        )
+        assertThat(viewModel.event.value).isEqualTo(expectedEvent)
 
         // Invoked once during loadOrders() and once during onBulkOrderStatusChanged()
         verify(viewModel.ordersPagedListWrapper, times(2))?.fetchFirstPage()

We here force sending a different instance of PagedList by the second mock() call, which triggers the SnackBar.


// First enter selection mode
viewModel.onSelectionChanged(1)
assertThat(viewModel.isSelecting()).isTrue()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np, just a remark about these assertions you have here as part of the Given part, I don't think they are needed because you already have a test case specific for the selection mode, and generally we assert as part of the Then section, so they feel out of place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature: order list Related to the order list. type: enhancement A request for an enhancement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants