Skip to content
This repository was archived by the owner on Feb 4, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.model.WCOrderListDescriptor
import org.wordpress.android.fluxc.model.WCOrderStatusModel
import org.wordpress.android.fluxc.model.WCOrderSummaryModel
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.BatchOrderApiResponse
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.CoreOrderStatus
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.CoreOrderStatus.COMPLETED
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderDto
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderRestClient
import org.wordpress.android.fluxc.persistence.OrderSqlUtils
import org.wordpress.android.fluxc.persistence.WCAndroidDatabase
Expand All @@ -46,6 +49,7 @@ import org.wordpress.android.fluxc.persistence.dao.OrdersDaoDecorator
import org.wordpress.android.fluxc.store.InsertOrder
import org.wordpress.android.fluxc.store.WCOrderFetcher
import org.wordpress.android.fluxc.store.WCOrderStore
import org.wordpress.android.fluxc.store.WCOrderStore.BulkUpdateOrderStatusResponsePayload
import org.wordpress.android.fluxc.store.WCOrderStore.FetchHasOrdersResponsePayload
import org.wordpress.android.fluxc.store.WCOrderStore.FetchOrderListResponsePayload
import org.wordpress.android.fluxc.store.WCOrderStore.FetchOrderStatusOptionsResponsePayload
Expand Down Expand Up @@ -605,6 +609,109 @@ class WCOrderStoreTest {
}
}

@Test
fun `given successful response for all orders when batch updating status then returns successful orders`() {
runBlocking {
// Given
val site = SiteModel().apply { id = 1 }
val orderIds = listOf(1L, 2L, 3L)
val newStatus = COMPLETED.value

// Create mocked OrderDto objects for success responses
val order1 = mock<OrderDto>().apply {
whenever(id).thenReturn(1L)
whenever(status).thenReturn(COMPLETED.value)
}
val order2 = mock<OrderDto>().apply {
whenever(id).thenReturn(2L)
whenever(status).thenReturn(COMPLETED.value)
}
val order3 = mock<OrderDto>().apply {
whenever(id).thenReturn(3L)
whenever(status).thenReturn(COMPLETED.value)
}

val successResponses = listOf(
BatchOrderApiResponse.OrderResponse.Success(order1),
BatchOrderApiResponse.OrderResponse.Success(order2),
BatchOrderApiResponse.OrderResponse.Success(order3)
)

whenever(orderRestClient.batchUpdateOrdersStatus(site, orderIds, newStatus))
.thenReturn(BulkUpdateOrderStatusResponsePayload(successResponses))

// When
val result = orderStore.batchUpdateOrdersStatus(
site,
orderIds,
WCOrderStatusModel(COMPLETED.value)
)

// Then
assertThat(result.isError).isFalse()
result.model?.let { updateResult ->
assertEquals(orderIds, updateResult.updatedOrders)
assertTrue(updateResult.failedOrders.isEmpty())
}
}
}

@Test
fun `given mixed response when batch updating status then returns successful and failed orders`() {
runBlocking {
// Given
val site = SiteModel().apply { id = 1 }
val orderIds = listOf(1L, 2L, 3L)
val newStatus = COMPLETED.value

// Mock successful orders
val order1 = mock<OrderDto>().apply {
whenever(id).thenReturn(1L)
whenever(status).thenReturn(COMPLETED.value)
}
val order3 = mock<OrderDto>().apply {
whenever(id).thenReturn(3L)
whenever(status).thenReturn(COMPLETED.value)
}

val mixedResponses = listOf(
BatchOrderApiResponse.OrderResponse.Success(order1),
BatchOrderApiResponse.OrderResponse.Error(
id = 2L,
error = BatchOrderApiResponse.ErrorResponse(
code = "woocommerce_rest_shop_order_invalid_id",
message = "Invalid ID.",
data = BatchOrderApiResponse.ErrorData(status = 400)
)
),
BatchOrderApiResponse.OrderResponse.Success(order3)
)

whenever(orderRestClient.batchUpdateOrdersStatus(site, orderIds, newStatus))
.thenReturn(BulkUpdateOrderStatusResponsePayload(mixedResponses))

// When
val result = orderStore.batchUpdateOrdersStatus(
site,
orderIds,
WCOrderStatusModel(COMPLETED.value)
)

// Then
assertThat(result.isError).isFalse()
result.model?.let { updateResult ->
assertEquals(listOf(1L, 3L), updateResult.updatedOrders)
assertEquals(1, updateResult.failedOrders.size)
with(updateResult.failedOrders[0]) {
assertEquals(2L, id)
assertEquals("woocommerce_rest_shop_order_invalid_id", errorCode)
assertEquals("Invalid ID.", errorMessage)
assertEquals(400, errorStatus)
}
}
}
}

private fun setupMissingOrders(): MutableMap<WCOrderSummaryModel, OrderEntity?> {
return mutableMapOf<WCOrderSummaryModel, OrderEntity?>().apply {
(21L..30L).forEach { index ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@ import com.google.gson.annotations.JsonAdapter
import java.lang.reflect.Type
import org.wordpress.android.fluxc.network.Response

/**
* Represents the response from WooCommerce's Batch Order Update API endpoint.
* https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#batch-update-orders
*
* While the WooCommerce REST API orders batch endpoint supports three operations at once
* (create, update, delete), this class specifically handles only the "update" operation
* responses, because we don't yet support the other operations.
*
* The response contains a list of order updates, where each update can be
* either successful or failed.
* 1. Success: Contains the complete updated order data (OrderDto)
* 2. Error: Contains the failed order ID and error details
*
* Also refer to the orders-batch.json file in test resources.
*
* Example successful response:
* {
* "update": [{
* "id": 1032,
* "status": "completed",
* // ... other order fields
* }]
* }
*
* Example error response:
* {
* "update": [{
* "id": "525",
* "error": {
* "code": "woocommerce_rest_shop_order_invalid_id",
* "message": "Invalid ID.",
* "data": { "status": 400 }
* }
* }]
* }
*
*/
data class BatchOrderApiResponse(
val update: List<OrderResponse>
) : Response {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.wordpress.android.fluxc.network.rest.wpcom.wc.toWooError
import org.wordpress.android.fluxc.persistence.entity.OrderNoteEntity
import org.wordpress.android.fluxc.store.WCOrderStore
import org.wordpress.android.fluxc.store.WCOrderStore.AddOrderShipmentTrackingResponsePayload
import org.wordpress.android.fluxc.store.WCOrderStore.BulkUpdateOrderStatusResponsePayload
import org.wordpress.android.fluxc.store.WCOrderStore.DeleteOrderShipmentTrackingResponsePayload
import org.wordpress.android.fluxc.store.WCOrderStore.FetchHasOrdersResponsePayload
import org.wordpress.android.fluxc.store.WCOrderStore.FetchOrderListResponsePayload
Expand Down Expand Up @@ -1073,6 +1074,63 @@ class OrderRestClient @Inject constructor(
}
}

/**
* Performs a batch update of order statuses via the WooCommerce REST API.
*
* This endpoint enables updating multiple orders to the same status in a single network request.
* The WooCommerce API has a limit of 100 orders per batch update.
*
* @param site The site to perform the update on
* @param orderIds List of order IDs to update. Error if exceeds [BATCH_UPDATE_LIMIT]
* @param newStatus The new status to set for all specified orders
* @return [BulkUpdateOrderStatusResponsePayload] containing either the update results or an error
*/
suspend fun batchUpdateOrdersStatus(
site: SiteModel,
orderIds: List<Long>,
newStatus: String
): BulkUpdateOrderStatusResponsePayload {
// Check batch update limit
if (orderIds.size > BATCH_UPDATE_LIMIT) {
return BulkUpdateOrderStatusResponsePayload(
error = OrderError(
type = OrderErrorType.BULK_UPDATE_LIMIT_EXCEEDED,
message = "Cannot update more than 100 orders at once"
)
)
}

val url = WOOCOMMERCE.orders.batch.pathV3
val updateRequests = orderIds.map { orderId ->
mapOf(
"id" to orderId,
"status" to newStatus
)
}

val response = wooNetwork.executePostGsonRequest(
site = site,
path = url,
clazz = BatchOrderApiResponse::class.java,
body = mapOf("update" to updateRequests)
)

return when (response) {
is WPAPIResponse.Success -> {
response.data?.let {
BulkUpdateOrderStatusResponsePayload(it.update)
} ?: BulkUpdateOrderStatusResponsePayload(
OrderError(GENERIC_ERROR, "Success response with empty data")
)
}

is WPAPIResponse.Error -> {
val orderError = wpAPINetworkErrorToOrderError(response.error)
BulkUpdateOrderStatusResponsePayload(orderError)
}
}
}

private fun UpdateOrderRequest.toNetworkRequest(): Map<String, Any> {
return mutableMapOf<String, Any>().apply {
customerId?.let { put("customer_id", it) }
Expand Down Expand Up @@ -1202,6 +1260,8 @@ class OrderRestClient @Inject constructor(
"tracking_number",
"tracking_provider"
).joinToString(separator = ",")

private const val BATCH_UPDATE_LIMIT = 100
}

enum class SortOrder(val value: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType.SERVER_E
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooError
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType.API_ERROR
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.BatchOrderApiResponse
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderRestClient
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderRestClient.OrderBy
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.OrderRestClient.SortOrder
Expand All @@ -43,6 +44,7 @@ import org.wordpress.android.fluxc.store.WCOrderStore.OrderErrorType.PARSE_ERROR
import org.wordpress.android.fluxc.store.WCOrderStore.OrderErrorType.TIMEOUT_ERROR
import org.wordpress.android.fluxc.store.WCOrderStore.UpdateOrderResult.OptimisticUpdateResult
import org.wordpress.android.fluxc.store.WCOrderStore.UpdateOrderResult.RemoteUpdateResult
import org.wordpress.android.fluxc.store.WCOrderStore.UpdateOrdersStatusResult.FailedOrder
import org.wordpress.android.fluxc.tools.CoroutineEngine
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T.API
Expand Down Expand Up @@ -297,6 +299,26 @@ class WCOrderStore @Inject constructor(
}
}

class BulkUpdateOrderStatusResponsePayload(
val response: List<BatchOrderApiResponse.OrderResponse>
) : Payload<OrderError>() {
constructor(error: OrderError) : this(emptyList()) {
this.error = error
}
}

data class UpdateOrdersStatusResult(
val updatedOrders: List<Long> = emptyList(),
val failedOrders: List<FailedOrder> = emptyList()
) {
data class FailedOrder(
val id: Long,
val errorCode: String,
val errorMessage: String,
val errorStatus: Int
)
}

data class OrderError(val type: OrderErrorType = GENERIC_ERROR, val message: String = "") : OnChangedError

enum class OrderErrorType {
Expand All @@ -308,7 +330,8 @@ class WCOrderStore @Inject constructor(
GENERIC_ERROR,
PARSE_ERROR,
TIMEOUT_ERROR,
EMPTY_BILLING_EMAIL;
EMPTY_BILLING_EMAIL,
BULK_UPDATE_LIMIT_EXCEEDED;

companion object {
private val reverseMap = values().associateBy(OrderErrorType::name)
Expand Down Expand Up @@ -1146,4 +1169,42 @@ class WCOrderStore @Inject constructor(
WooResult(orders)
}
}

@Suppress("NestedBlockDepth")
suspend fun batchUpdateOrdersStatus(
site: SiteModel,
orderIds: List<Long>,
newStatus: WCOrderStatusModel
): WooResult<UpdateOrdersStatusResult> {
val result = wcOrderRestClient.batchUpdateOrdersStatus(site, orderIds, newStatus.statusKey)

return if (!result.isError) {
val orders = result.response
val updatedOrders = mutableListOf<Long>()
val failedOrders = mutableListOf<FailedOrder>()

orders.forEach { response ->
when (response) {
is BatchOrderApiResponse.OrderResponse.Success -> {
response.order.id?.let { updatedOrders.add(it) }
}

is BatchOrderApiResponse.OrderResponse.Error -> {
failedOrders.add(
FailedOrder(
id = response.id,
errorCode = response.error.code,
errorMessage = response.error.message,
errorStatus = response.error.data.status
)
)
}
}
}

WooResult(UpdateOrdersStatusResult(updatedOrders, failedOrders))
} else {
WooResult(WooError(API_ERROR, SERVER_ERROR, result.error.message))
}
}
}
Loading