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

Commit

Permalink
Merge pull request #3120 from wordpress-mobile/woo/orders-batch-endpo…
Browse files Browse the repository at this point in the history
…int-p2

Woo: Part two for orders/batch endpoint
  • Loading branch information
hafizrahman authored Dec 16, 2024
2 parents e32b77f + 3ba4738 commit 242d0b6
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 1 deletion.
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))
}
}
}

0 comments on commit 242d0b6

Please sign in to comment.