Skip to content
This repository was archived by the owner on Feb 4, 2025. It is now read-only.

Commit 242d0b6

Browse files
authored
Merge pull request #3120 from wordpress-mobile/woo/orders-batch-endpoint-p2
Woo: Part two for orders/batch endpoint
2 parents e32b77f + 3ba4738 commit 242d0b6

File tree

4 files changed

+266
-1
lines changed

4 files changed

+266
-1
lines changed

example/src/test/java/org/wordpress/android/fluxc/wc/order/WCOrderStoreTest.kt

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ import org.wordpress.android.fluxc.model.SiteModel
3535
import org.wordpress.android.fluxc.model.WCOrderListDescriptor
3636
import org.wordpress.android.fluxc.model.WCOrderStatusModel
3737
import org.wordpress.android.fluxc.model.WCOrderSummaryModel
38+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.BatchOrderApiResponse
3839
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.CoreOrderStatus
40+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.CoreOrderStatus.COMPLETED
41+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderDto
3942
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderRestClient
4043
import org.wordpress.android.fluxc.persistence.OrderSqlUtils
4144
import org.wordpress.android.fluxc.persistence.WCAndroidDatabase
@@ -46,6 +49,7 @@ import org.wordpress.android.fluxc.persistence.dao.OrdersDaoDecorator
4649
import org.wordpress.android.fluxc.store.InsertOrder
4750
import org.wordpress.android.fluxc.store.WCOrderFetcher
4851
import org.wordpress.android.fluxc.store.WCOrderStore
52+
import org.wordpress.android.fluxc.store.WCOrderStore.BulkUpdateOrderStatusResponsePayload
4953
import org.wordpress.android.fluxc.store.WCOrderStore.FetchHasOrdersResponsePayload
5054
import org.wordpress.android.fluxc.store.WCOrderStore.FetchOrderListResponsePayload
5155
import org.wordpress.android.fluxc.store.WCOrderStore.FetchOrderStatusOptionsResponsePayload
@@ -605,6 +609,109 @@ class WCOrderStoreTest {
605609
}
606610
}
607611

612+
@Test
613+
fun `given successful response for all orders when batch updating status then returns successful orders`() {
614+
runBlocking {
615+
// Given
616+
val site = SiteModel().apply { id = 1 }
617+
val orderIds = listOf(1L, 2L, 3L)
618+
val newStatus = COMPLETED.value
619+
620+
// Create mocked OrderDto objects for success responses
621+
val order1 = mock<OrderDto>().apply {
622+
whenever(id).thenReturn(1L)
623+
whenever(status).thenReturn(COMPLETED.value)
624+
}
625+
val order2 = mock<OrderDto>().apply {
626+
whenever(id).thenReturn(2L)
627+
whenever(status).thenReturn(COMPLETED.value)
628+
}
629+
val order3 = mock<OrderDto>().apply {
630+
whenever(id).thenReturn(3L)
631+
whenever(status).thenReturn(COMPLETED.value)
632+
}
633+
634+
val successResponses = listOf(
635+
BatchOrderApiResponse.OrderResponse.Success(order1),
636+
BatchOrderApiResponse.OrderResponse.Success(order2),
637+
BatchOrderApiResponse.OrderResponse.Success(order3)
638+
)
639+
640+
whenever(orderRestClient.batchUpdateOrdersStatus(site, orderIds, newStatus))
641+
.thenReturn(BulkUpdateOrderStatusResponsePayload(successResponses))
642+
643+
// When
644+
val result = orderStore.batchUpdateOrdersStatus(
645+
site,
646+
orderIds,
647+
WCOrderStatusModel(COMPLETED.value)
648+
)
649+
650+
// Then
651+
assertThat(result.isError).isFalse()
652+
result.model?.let { updateResult ->
653+
assertEquals(orderIds, updateResult.updatedOrders)
654+
assertTrue(updateResult.failedOrders.isEmpty())
655+
}
656+
}
657+
}
658+
659+
@Test
660+
fun `given mixed response when batch updating status then returns successful and failed orders`() {
661+
runBlocking {
662+
// Given
663+
val site = SiteModel().apply { id = 1 }
664+
val orderIds = listOf(1L, 2L, 3L)
665+
val newStatus = COMPLETED.value
666+
667+
// Mock successful orders
668+
val order1 = mock<OrderDto>().apply {
669+
whenever(id).thenReturn(1L)
670+
whenever(status).thenReturn(COMPLETED.value)
671+
}
672+
val order3 = mock<OrderDto>().apply {
673+
whenever(id).thenReturn(3L)
674+
whenever(status).thenReturn(COMPLETED.value)
675+
}
676+
677+
val mixedResponses = listOf(
678+
BatchOrderApiResponse.OrderResponse.Success(order1),
679+
BatchOrderApiResponse.OrderResponse.Error(
680+
id = 2L,
681+
error = BatchOrderApiResponse.ErrorResponse(
682+
code = "woocommerce_rest_shop_order_invalid_id",
683+
message = "Invalid ID.",
684+
data = BatchOrderApiResponse.ErrorData(status = 400)
685+
)
686+
),
687+
BatchOrderApiResponse.OrderResponse.Success(order3)
688+
)
689+
690+
whenever(orderRestClient.batchUpdateOrdersStatus(site, orderIds, newStatus))
691+
.thenReturn(BulkUpdateOrderStatusResponsePayload(mixedResponses))
692+
693+
// When
694+
val result = orderStore.batchUpdateOrdersStatus(
695+
site,
696+
orderIds,
697+
WCOrderStatusModel(COMPLETED.value)
698+
)
699+
700+
// Then
701+
assertThat(result.isError).isFalse()
702+
result.model?.let { updateResult ->
703+
assertEquals(listOf(1L, 3L), updateResult.updatedOrders)
704+
assertEquals(1, updateResult.failedOrders.size)
705+
with(updateResult.failedOrders[0]) {
706+
assertEquals(2L, id)
707+
assertEquals("woocommerce_rest_shop_order_invalid_id", errorCode)
708+
assertEquals("Invalid ID.", errorMessage)
709+
assertEquals(400, errorStatus)
710+
}
711+
}
712+
}
713+
}
714+
608715
private fun setupMissingOrders(): MutableMap<WCOrderSummaryModel, OrderEntity?> {
609716
return mutableMapOf<WCOrderSummaryModel, OrderEntity?>().apply {
610717
(21L..30L).forEach { index ->

plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/order/BatchOrderApiResponse.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,43 @@ import com.google.gson.annotations.JsonAdapter
77
import java.lang.reflect.Type
88
import org.wordpress.android.fluxc.network.Response
99

10+
/**
11+
* Represents the response from WooCommerce's Batch Order Update API endpoint.
12+
* https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#batch-update-orders
13+
*
14+
* While the WooCommerce REST API orders batch endpoint supports three operations at once
15+
* (create, update, delete), this class specifically handles only the "update" operation
16+
* responses, because we don't yet support the other operations.
17+
*
18+
* The response contains a list of order updates, where each update can be
19+
* either successful or failed.
20+
* 1. Success: Contains the complete updated order data (OrderDto)
21+
* 2. Error: Contains the failed order ID and error details
22+
*
23+
* Also refer to the orders-batch.json file in test resources.
24+
*
25+
* Example successful response:
26+
* {
27+
* "update": [{
28+
* "id": 1032,
29+
* "status": "completed",
30+
* // ... other order fields
31+
* }]
32+
* }
33+
*
34+
* Example error response:
35+
* {
36+
* "update": [{
37+
* "id": "525",
38+
* "error": {
39+
* "code": "woocommerce_rest_shop_order_invalid_id",
40+
* "message": "Invalid ID.",
41+
* "data": { "status": 400 }
42+
* }
43+
* }]
44+
* }
45+
*
46+
*/
1047
data class BatchOrderApiResponse(
1148
val update: List<OrderResponse>
1249
) : Response {

plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/order/OrderRestClient.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import org.wordpress.android.fluxc.network.rest.wpcom.wc.toWooError
3030
import org.wordpress.android.fluxc.persistence.entity.OrderNoteEntity
3131
import org.wordpress.android.fluxc.store.WCOrderStore
3232
import org.wordpress.android.fluxc.store.WCOrderStore.AddOrderShipmentTrackingResponsePayload
33+
import org.wordpress.android.fluxc.store.WCOrderStore.BulkUpdateOrderStatusResponsePayload
3334
import org.wordpress.android.fluxc.store.WCOrderStore.DeleteOrderShipmentTrackingResponsePayload
3435
import org.wordpress.android.fluxc.store.WCOrderStore.FetchHasOrdersResponsePayload
3536
import org.wordpress.android.fluxc.store.WCOrderStore.FetchOrderListResponsePayload
@@ -1073,6 +1074,63 @@ class OrderRestClient @Inject constructor(
10731074
}
10741075
}
10751076

1077+
/**
1078+
* Performs a batch update of order statuses via the WooCommerce REST API.
1079+
*
1080+
* This endpoint enables updating multiple orders to the same status in a single network request.
1081+
* The WooCommerce API has a limit of 100 orders per batch update.
1082+
*
1083+
* @param site The site to perform the update on
1084+
* @param orderIds List of order IDs to update. Error if exceeds [BATCH_UPDATE_LIMIT]
1085+
* @param newStatus The new status to set for all specified orders
1086+
* @return [BulkUpdateOrderStatusResponsePayload] containing either the update results or an error
1087+
*/
1088+
suspend fun batchUpdateOrdersStatus(
1089+
site: SiteModel,
1090+
orderIds: List<Long>,
1091+
newStatus: String
1092+
): BulkUpdateOrderStatusResponsePayload {
1093+
// Check batch update limit
1094+
if (orderIds.size > BATCH_UPDATE_LIMIT) {
1095+
return BulkUpdateOrderStatusResponsePayload(
1096+
error = OrderError(
1097+
type = OrderErrorType.BULK_UPDATE_LIMIT_EXCEEDED,
1098+
message = "Cannot update more than 100 orders at once"
1099+
)
1100+
)
1101+
}
1102+
1103+
val url = WOOCOMMERCE.orders.batch.pathV3
1104+
val updateRequests = orderIds.map { orderId ->
1105+
mapOf(
1106+
"id" to orderId,
1107+
"status" to newStatus
1108+
)
1109+
}
1110+
1111+
val response = wooNetwork.executePostGsonRequest(
1112+
site = site,
1113+
path = url,
1114+
clazz = BatchOrderApiResponse::class.java,
1115+
body = mapOf("update" to updateRequests)
1116+
)
1117+
1118+
return when (response) {
1119+
is WPAPIResponse.Success -> {
1120+
response.data?.let {
1121+
BulkUpdateOrderStatusResponsePayload(it.update)
1122+
} ?: BulkUpdateOrderStatusResponsePayload(
1123+
OrderError(GENERIC_ERROR, "Success response with empty data")
1124+
)
1125+
}
1126+
1127+
is WPAPIResponse.Error -> {
1128+
val orderError = wpAPINetworkErrorToOrderError(response.error)
1129+
BulkUpdateOrderStatusResponsePayload(orderError)
1130+
}
1131+
}
1132+
}
1133+
10761134
private fun UpdateOrderRequest.toNetworkRequest(): Map<String, Any> {
10771135
return mutableMapOf<String, Any>().apply {
10781136
customerId?.let { put("customer_id", it) }
@@ -1202,6 +1260,8 @@ class OrderRestClient @Inject constructor(
12021260
"tracking_number",
12031261
"tracking_provider"
12041262
).joinToString(separator = ",")
1263+
1264+
private const val BATCH_UPDATE_LIMIT = 100
12051265
}
12061266

12071267
enum class SortOrder(val value: String) {

plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCOrderStore.kt

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType.SERVER_E
2626
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooError
2727
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType.API_ERROR
2828
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult
29+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.BatchOrderApiResponse
2930
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderRestClient
3031
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderRestClient.OrderBy
3132
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderRestClient.SortOrder
@@ -43,6 +44,7 @@ import org.wordpress.android.fluxc.store.WCOrderStore.OrderErrorType.PARSE_ERROR
4344
import org.wordpress.android.fluxc.store.WCOrderStore.OrderErrorType.TIMEOUT_ERROR
4445
import org.wordpress.android.fluxc.store.WCOrderStore.UpdateOrderResult.OptimisticUpdateResult
4546
import org.wordpress.android.fluxc.store.WCOrderStore.UpdateOrderResult.RemoteUpdateResult
47+
import org.wordpress.android.fluxc.store.WCOrderStore.UpdateOrdersStatusResult.FailedOrder
4648
import org.wordpress.android.fluxc.tools.CoroutineEngine
4749
import org.wordpress.android.util.AppLog
4850
import org.wordpress.android.util.AppLog.T.API
@@ -297,6 +299,26 @@ class WCOrderStore @Inject constructor(
297299
}
298300
}
299301

302+
class BulkUpdateOrderStatusResponsePayload(
303+
val response: List<BatchOrderApiResponse.OrderResponse>
304+
) : Payload<OrderError>() {
305+
constructor(error: OrderError) : this(emptyList()) {
306+
this.error = error
307+
}
308+
}
309+
310+
data class UpdateOrdersStatusResult(
311+
val updatedOrders: List<Long> = emptyList(),
312+
val failedOrders: List<FailedOrder> = emptyList()
313+
) {
314+
data class FailedOrder(
315+
val id: Long,
316+
val errorCode: String,
317+
val errorMessage: String,
318+
val errorStatus: Int
319+
)
320+
}
321+
300322
data class OrderError(val type: OrderErrorType = GENERIC_ERROR, val message: String = "") : OnChangedError
301323

302324
enum class OrderErrorType {
@@ -308,7 +330,8 @@ class WCOrderStore @Inject constructor(
308330
GENERIC_ERROR,
309331
PARSE_ERROR,
310332
TIMEOUT_ERROR,
311-
EMPTY_BILLING_EMAIL;
333+
EMPTY_BILLING_EMAIL,
334+
BULK_UPDATE_LIMIT_EXCEEDED;
312335

313336
companion object {
314337
private val reverseMap = values().associateBy(OrderErrorType::name)
@@ -1146,4 +1169,42 @@ class WCOrderStore @Inject constructor(
11461169
WooResult(orders)
11471170
}
11481171
}
1172+
1173+
@Suppress("NestedBlockDepth")
1174+
suspend fun batchUpdateOrdersStatus(
1175+
site: SiteModel,
1176+
orderIds: List<Long>,
1177+
newStatus: WCOrderStatusModel
1178+
): WooResult<UpdateOrdersStatusResult> {
1179+
val result = wcOrderRestClient.batchUpdateOrdersStatus(site, orderIds, newStatus.statusKey)
1180+
1181+
return if (!result.isError) {
1182+
val orders = result.response
1183+
val updatedOrders = mutableListOf<Long>()
1184+
val failedOrders = mutableListOf<FailedOrder>()
1185+
1186+
orders.forEach { response ->
1187+
when (response) {
1188+
is BatchOrderApiResponse.OrderResponse.Success -> {
1189+
response.order.id?.let { updatedOrders.add(it) }
1190+
}
1191+
1192+
is BatchOrderApiResponse.OrderResponse.Error -> {
1193+
failedOrders.add(
1194+
FailedOrder(
1195+
id = response.id,
1196+
errorCode = response.error.code,
1197+
errorMessage = response.error.message,
1198+
errorStatus = response.error.data.status
1199+
)
1200+
)
1201+
}
1202+
}
1203+
}
1204+
1205+
WooResult(UpdateOrdersStatusResult(updatedOrders, failedOrders))
1206+
} else {
1207+
WooResult(WooError(API_ERROR, SERVER_ERROR, result.error.message))
1208+
}
1209+
}
11491210
}

0 commit comments

Comments
 (0)