Skip to content

Shipping methods networking #11478

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

Merged
merged 11 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
@@ -1,10 +1,16 @@
package com.woocommerce.android.model

import android.os.Parcelable
import com.woocommerce.android.network.shippingmethods.ShippingMethodsRestClient
import com.woocommerce.android.ui.orders.creation.shipping.ShippingMethodsRepository
import kotlinx.parcelize.Parcelize

@Parcelize
data class ShippingMethod(
val id: String,
val title: String
) : Parcelable

fun ShippingMethodsRestClient.ShippingMethodDto.toAppModel(): ShippingMethod {
return ShippingMethod(id = this.id ?: ShippingMethodsRepository.OTHER_ID, title = this.title.orEmpty())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.woocommerce.android.network.shippingmethods

import org.wordpress.android.fluxc.generated.endpoint.WOOCOMMERCE
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooNetwork
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooPayload
import org.wordpress.android.fluxc.utils.toWooPayload
import javax.inject.Inject

class ShippingMethodsRestClient @Inject constructor(private val wooNetwork: WooNetwork) {
suspend fun fetchShippingMethods(site: SiteModel): WooPayload<List<ShippingMethodDto>> {
val url = WOOCOMMERCE.shipping_methods.pathV3

return wooNetwork.executeGetGsonRequest(
site = site,
path = url,
clazz = Array<ShippingMethodDto>::class.java,
).toWooPayload { methods -> methods.toList() }
}

data class ShippingMethodDto(
val id: String? = null,
val title: String? = null,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.woocommerce.android.ui.orders.creation.shipping

import com.woocommerce.android.model.ShippingMethod
import javax.inject.Inject

class GetShippingMethodById @Inject constructor(
private val shippingMethodsRepository: ShippingMethodsRepository
) {
suspend operator fun invoke(shippingMethodId: String?): ShippingMethod {
val other = shippingMethodsRepository.getOtherShippingMethod()
if (shippingMethodId == ShippingMethodsRepository.OTHER_ID || shippingMethodId == null) {
return other
}
val result = shippingMethodsRepository.fetchShippingMethods()
return result.model?.firstOrNull { shippingMethod -> shippingMethod.id == shippingMethodId } ?: other
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, does the API allow querying for a shipping method with a given ID? I'm wondering if we have a way to avoid fetching everything in a request only to filter it locally.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great point! I totally missed the endpoint at first 🤦 . The fix is here

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome! Thanks for revisiting the API and finding such a great solution! 🎉

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.woocommerce.android.ui.orders.creation.shipping

import com.woocommerce.android.model.ShippingMethod
import javax.inject.Inject

class GetShippingMethodsWithOtherValue @Inject constructor(
private val shippingMethodsRepository: ShippingMethodsRepository
) {
suspend operator fun invoke(): Result<List<ShippingMethod>> {
val result = shippingMethodsRepository.fetchShippingMethods()
return when {
result.model != null -> {
val shippingMethodsWithOtherValue = result.model!!.toMutableList().also {
it.add(shippingMethodsRepository.getOtherShippingMethod())
}

Result.success(shippingMethodsWithOtherValue)
}

else -> {
val message = result.error.message ?: "error fetching shipping methods"
Result.failure(Exception(message))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import com.woocommerce.android.viewmodel.MultiLiveEvent
import com.woocommerce.android.viewmodel.ScopedViewModel
import com.woocommerce.android.viewmodel.navArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class OrderShippingMethodsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
savedStateHandle: SavedStateHandle,
private val getShippingMethodsWithOtherValue: GetShippingMethodsWithOtherValue
) : ScopedViewModel(savedStateHandle) {
private val navArgs: OrderShippingMethodsFragmentArgs by savedState.navArgs()
val viewState: MutableStateFlow<ViewState>
Expand All @@ -33,33 +33,20 @@ class OrderShippingMethodsViewModel @Inject constructor(

private suspend fun getShippingMethods() {
viewState.value = ViewState.Loading
delay(1000)
val fetchedShippingMethods = listOf(
ShippingMethod(
id = "flat_rate",
title = "Flat Rate"
),
ShippingMethod(
id = "free_shipping",
title = "Free shipping"
),
ShippingMethod(
id = "local_pickup",
title = "Local pickup"
),
ShippingMethod(
id = "other",
title = "Other"
)
)

var methodsUIList = fetchedShippingMethods.map { ShippingMethodUI(it) }
getShippingMethodsWithOtherValue().fold(
onSuccess = { fetchedShippingMethods ->
var methodsUIList = fetchedShippingMethods.map { ShippingMethodUI(it) }

methodsUIList = navArgs.selectedMethodId?.let { selectedId ->
updateSelection(selectedId, fetchedShippingMethods.map { ShippingMethodUI(it) })
} ?: methodsUIList
methodsUIList = navArgs.selectedMethodId?.let { selectedId ->
updateSelection(selectedId, fetchedShippingMethods.map { ShippingMethodUI(it) })
} ?: methodsUIList

viewState.value = ViewState.ShippingMethodsState(methods = methodsUIList)
viewState.value = ViewState.ShippingMethodsState(methods = methodsUIList)
},
onFailure = {
viewState.value = ViewState.Error
}
)
}

fun onMethodSelected(selected: ShippingMethodUI) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ package com.woocommerce.android.ui.orders.creation.shipping
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import com.woocommerce.android.R
import com.woocommerce.android.extensions.capitalize
import com.woocommerce.android.model.Order
import com.woocommerce.android.model.ShippingMethod
import com.woocommerce.android.viewmodel.MultiLiveEvent
import com.woocommerce.android.viewmodel.ResourceProvider
import com.woocommerce.android.viewmodel.ScopedViewModel
import com.woocommerce.android.viewmodel.navArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
Expand All @@ -21,7 +19,8 @@ import javax.inject.Inject
@HiltViewModel
class OrderShippingViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val resourceProvider: ResourceProvider
private val resourceProvider: ResourceProvider,
private val getShippingMethodById: GetShippingMethodById
) : ScopedViewModel(savedStateHandle) {

private val navArgs: OrderShippingFragmentArgs by savedState.navArgs()
Expand All @@ -43,7 +42,7 @@ class OrderShippingViewModel @Inject constructor(
navArgs.currentShippingLine?.let { shippingLine: Order.ShippingLine ->
launch {
viewState.value = ViewState.ShippingState(
method = getShippingMethod(shippingLine),
method = getShippingMethodById(shippingLine.methodId),
name = shippingLine.methodTitle,
amount = shippingLine.total,
isEditFlow = true,
Expand All @@ -53,19 +52,6 @@ class OrderShippingViewModel @Inject constructor(
}
}

@Suppress("MagicNumber")
private suspend fun getShippingMethod(shippingLine: Order.ShippingLine): ShippingMethod? {
return if (shippingLine.methodId == null) {
null
} else {
delay(1000)
ShippingMethod(
id = shippingLine.methodId,
title = shippingLine.methodId.capitalize()
)
}
}

fun onNameChanged(name: String) {
(viewState.value as? ViewState.ShippingState)?.let {
viewState.value = it.copy(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.woocommerce.android.ui.orders.creation.shipping

import com.woocommerce.android.R
import com.woocommerce.android.model.ShippingMethod
import com.woocommerce.android.model.toAppModel
import com.woocommerce.android.network.shippingmethods.ShippingMethodsRestClient
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.util.CoroutineDispatchers
import com.woocommerce.android.viewmodel.ResourceProvider
import kotlinx.coroutines.withContext
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.BaseRequest
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooError
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult
import javax.inject.Inject

class ShippingMethodsRepository @Inject constructor(
private val selectedSite: SelectedSite,
private val shippingMethodsRestClient: ShippingMethodsRestClient,
private val resourceProvider: ResourceProvider,
private val dispatchers: CoroutineDispatchers
) {
companion object {
const val OTHER_ID = "other"
}

suspend fun fetchShippingMethods(site: SiteModel = selectedSite.get()): WooResult<List<ShippingMethod>> {
Copy link
Contributor

@ThomazFB ThomazFB May 10, 2024

Choose a reason for hiding this comment

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

Since this is a networking PR, I imagine this will be tackled later. But just to make sure: do you plan to add database support for the Methods after fetching? It could be beneficial to avoid requesting all the data every time one of the Use Cases is triggered, especially the ID one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It could be beneficial to avoid requesting all the data every time one of the Use Cases is triggered, especially the ID one.

Good point! I will add a task to improve this and rely more on local data in an upcoming PR

return withContext(dispatchers.io) {
val response = shippingMethodsRestClient.fetchShippingMethods(site)
when {
response.isError -> {
WooResult(response.error)
}

response.result != null -> {
val shippingMethods = response.result!!.map { dto -> dto.toAppModel() }
WooResult(shippingMethods)
}

else -> WooResult(WooError(WooErrorType.GENERIC_ERROR, BaseRequest.GenericErrorType.UNKNOWN))
}
}
}

fun getOtherShippingMethod(): ShippingMethod {
return ShippingMethod(
id = OTHER_ID,
title = resourceProvider.getString(R.string.other)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.woocommerce.android.ui.orders.creation.shipping

import com.woocommerce.android.network.shippingmethods.ShippingMethodsRestClient
import com.woocommerce.android.tools.SelectedSite
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.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.BaseRequest
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooError
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooPayload

@OptIn(ExperimentalCoroutinesApi::class)
class GetShippingMethodByIdTest : BaseUnitTest() {
private val siteModel = SiteModel()
private val selectedSite: SelectedSite = mock {
on { get() } doReturn siteModel
}
private val resourceProvider: ResourceProvider = mock()
private val shippingMethodsRestClient: ShippingMethodsRestClient = mock()

private val shippingMethodsRepository = ShippingMethodsRepository(
selectedSite = selectedSite,
dispatchers = coroutinesTestRule.testDispatchers,
resourceProvider = resourceProvider,
shippingMethodsRestClient = shippingMethodsRestClient
)

val sut = GetShippingMethodById(shippingMethodsRepository)

@Test
fun `when the method is in the result, then return is the expected`() = testBlocking {
val fetchResult = List(3) { i ->
ShippingMethodsRestClient.ShippingMethodDto(
id = "id$i",
title = "title$i",
)
}
whenever(shippingMethodsRestClient.fetchShippingMethods(siteModel)).doReturn(WooPayload(fetchResult))
whenever(resourceProvider.getString(any())).doReturn("Other")

val result = sut.invoke("id1")
assertThat(result).isNotNull
assertThat(result.id).isEqualTo("id1")
}

@Test
fun `when the method id is other, then return is the expected`() = testBlocking {
whenever(resourceProvider.getString(any())).doReturn("Other")

val result = sut.invoke(ShippingMethodsRepository.OTHER_ID)

// If is other doesn't need to fetch the values from the API
verify(shippingMethodsRestClient, never()).fetchShippingMethods(siteModel)
assertThat(result).isNotNull
assertThat(result.id).isEqualTo(ShippingMethodsRepository.OTHER_ID)
}

@Test
fun `when the method is NOT in the result, then return other`() = testBlocking {
val fetchResult = List(3) { i ->
ShippingMethodsRestClient.ShippingMethodDto(
id = "id$i",
title = "title$i",
)
}
whenever(shippingMethodsRestClient.fetchShippingMethods(siteModel)).doReturn(WooPayload(fetchResult))
whenever(resourceProvider.getString(any())).doReturn("Other")

val result = sut.invoke("id8")
assertThat(result).isNotNull
assertThat(result.id).isEqualTo(ShippingMethodsRepository.OTHER_ID)
}

@Test
fun `when fetching shipping methods fail, then return other`() = testBlocking {
val fetchResult = WooError(WooErrorType.GENERIC_ERROR, BaseRequest.GenericErrorType.UNKNOWN)
whenever(shippingMethodsRestClient.fetchShippingMethods(siteModel)).doReturn(WooPayload(fetchResult))
whenever(resourceProvider.getString(any())).doReturn("Other")

val result = sut.invoke("id8")
assertThat(result).isNotNull
assertThat(result.id).isEqualTo(ShippingMethodsRepository.OTHER_ID)
}
}
Loading
Loading