Skip to content

Commit

Permalink
Merge pull request #13648 from woocommerce/issue/12440-verify-destina…
Browse files Browse the repository at this point in the history
…tion-address

Verify destination address
  • Loading branch information
irfano authored Mar 5, 2025
2 parents 0a816f9 + 1ecb93c commit 01c6d89
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreat
import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.CustomsState.Unavailable
import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.PackageSelectionState.DataAvailable
import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.PackageSelectionState.NotSelected
import com.woocommerce.android.ui.orders.wooshippinglabels.address.destination.VerifyDestinationAddress
import com.woocommerce.android.ui.orders.wooshippinglabels.address.origin.FetchOriginAddresses
import com.woocommerce.android.ui.orders.wooshippinglabels.address.origin.ObserveOriginAddresses
import com.woocommerce.android.ui.orders.wooshippinglabels.customs.ShouldRequireCustomsForm
Expand Down Expand Up @@ -64,12 +65,14 @@ class WooShippingLabelCreationViewModel @Inject constructor(
private val purchaseShippingLabel: PurchaseShippingLabel,
private val observeStoreOptions: ObserveStoreOptions,
private val fetchAccountSettings: FetchAccountSettings,
private val shouldRequireCustoms: ShouldRequireCustomsForm
private val shouldRequireCustoms: ShouldRequireCustomsForm,
private val verifyDestinationAddress: VerifyDestinationAddress
) : ScopedViewModel(savedState) {
private val navArgs: WooShippingLabelCreationFragmentArgs by savedState.navArgs()

private val emptyOrder = Order.getEmptyOrder(Date(), Date())
private val order = MutableStateFlow<Order>(emptyOrder)
private val destinationAddress = MutableStateFlow<DestinationShippingAddress>(DestinationShippingAddress.EMPTY)
private val shippingAddresses = MutableStateFlow<WooShippingAddresses?>(WooShippingAddresses.EMPTY)
private val loadTrigger = MutableSharedFlow<Unit>()
private val storeOptions = MutableStateFlow<StoreOptionsModel?>(StoreOptionsModel.EMPTY)
Expand Down Expand Up @@ -112,6 +115,7 @@ class WooShippingLabelCreationViewModel @Inject constructor(
init {
launch { observeShippingLabelInformation() }
launch { getStoreOptions() }
launch { getDestinationAddress() }
launch { getShippingAddresses() }
launch { getOrderInformation() }
launch { observePackageWeight() }
Expand All @@ -130,10 +134,26 @@ class WooShippingLabelCreationViewModel @Inject constructor(
}
}

private fun getStoreOptions() {
launch {
observeStoreOptions().collectLatest { options ->
storeOptions.value = options
private suspend fun getStoreOptions() {
observeStoreOptions().collectLatest { options ->
storeOptions.value = options
}
}

private suspend fun getDestinationAddress() {
order.drop(1).collectLatest { order ->
val defaultDestination = DestinationShippingAddress(
address = order.shippingAddress.copy(email = order.billingAddress.email),
isVerified = false
)

destinationAddress.value = defaultDestination

if (order.shippingAddress != Address.EMPTY) {
verifyDestinationAddress(order.id).fold(
onSuccess = { destinationAddress.value = it },
onFailure = { }
)
}
}
}
Expand Down Expand Up @@ -244,13 +264,13 @@ class WooShippingLabelCreationViewModel @Inject constructor(
}

private suspend fun getShippingAddresses() {
combine(order, observeOriginAddresses()) { order, originAddresses ->
combine(destinationAddress, observeOriginAddresses()) { destination, originAddresses ->
if (!originAddresses.isNullOrEmpty()) {
val selectedOriginAddress = getSelectedOriginAddress(originAddresses)
WooShippingAddresses(
shipFrom = selectedOriginAddress,
originAddresses = originAddresses,
shipTo = DestinationShippingAddress(order.shippingAddress, false)
shipTo = destination
)
} else {
null
Expand Down Expand Up @@ -353,10 +373,8 @@ class WooShippingLabelCreationViewModel @Inject constructor(
)
}

fun onUpdateDestinationAddress(destinationAddress: DestinationShippingAddress) {
shippingAddresses.value?.let {
shippingAddresses.value = it.copy(shipTo = destinationAddress)
}
fun onUpdateDestinationAddress(updatedDestinationAddress: DestinationShippingAddress) {
destinationAddress.value = updatedDestinationAddress
}

fun onRefreshShippingRates() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.woocommerce.android.ui.orders.wooshippinglabels.address.destination

import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.orders.wooshippinglabels.models.DestinationShippingAddress
import com.woocommerce.android.ui.orders.wooshippinglabels.networking.WooShippingLabelRepository
import com.woocommerce.android.util.CoroutineDispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject

class VerifyDestinationAddress @Inject constructor(
private val shippingRepository: WooShippingLabelRepository,
private val selectedSite: SelectedSite,
private val coroutineDispatchers: CoroutineDispatchers
) {
suspend operator fun invoke(
orderId: Long
): Result<DestinationShippingAddress> {
return withContext(coroutineDispatchers.io) {
selectedSite.getOrNull()?.let {
val response = shippingRepository.verifyDestinationAddress(it, orderId)
val result = response.model
when {
response.isError || result == null -> {
val message =
response.error.message
?: if (result == null) "Empty response" else "Unknown error"
Result.failure(Exception(message))
}

else -> Result.success(result)
}
} ?: Result.failure(Exception("No site selected"))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import javax.inject.Inject

class ShouldRequireCustomsForm @Inject constructor() {
operator fun invoke(addressData: WooShippingAddresses): Boolean {
if (addressData.isDifferentCountryShipment) return true
val isCountryEmpty = addressData.shipTo.address.country.code.isEmpty()
val isDifferentCountry = addressData.isDifferentCountryShipment

if (isCountryEmpty.not() && isDifferentCountry) return true

val isOriginAddressMilitary = isAddressInMilitaryState(
addressData.shipFrom.country,
addressData.shipFrom.state.orEmpty()
)

val isShippingAddressMilitary = isAddressInMilitaryState(
addressData.shipTo.address.country.code,
addressData.shipTo.address.state.codeOrRaw
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,9 @@ data class UpdateAddressResponseDTO(
val address: AddressDTO,
val isVerified: Boolean
)

data class VerifyDestinationAddressResponseDTO(
val success: Boolean,
val normalizedAddress: AddressDTO,
val isVerified: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,31 @@ class WooShippingLabelRepository @Inject constructor(
)
}
}

suspend fun verifyDestinationAddress(
site: SiteModel,
orderId: Long,
): WooResult<DestinationShippingAddress> {
val verifyDestinationAddress = restClient.verifyDestinationAddress(
site = site,
orderId = orderId,
)

return if (verifyDestinationAddress.result?.success == true) {
verifyDestinationAddress.asWooResult {
DestinationShippingAddress(
address = mapper(it.normalizedAddress),
isVerified = it.isVerified
)
}
} else {
WooResult(
WooError(
type = WooErrorType.INVALID_RESPONSE,
original = GenericErrorType.INVALID_RESPONSE,
message = "Address verification failed"
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,17 @@ class WooShippingLabelRestClient @Inject constructor(

return result.toWooPayload()
}

suspend fun verifyDestinationAddress(
site: SiteModel,
orderId: Long,
): WooPayload<VerifyDestinationAddressResponseDTO> {
val url = "/wcshipping/v1/address/$orderId/verify_order/"

return wooNetwork.executeGetGsonRequest(
site = site,
path = url,
clazz = VerifyDestinationAddressResponseDTO::class.java,
).toWooPayload()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreat
import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.PurchaseState
import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.WooShippingViewState
import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.WooShippingViewState.DataState
import com.woocommerce.android.ui.orders.wooshippinglabels.address.destination.VerifyDestinationAddress
import com.woocommerce.android.ui.orders.wooshippinglabels.address.origin.ObserveOriginAddresses
import com.woocommerce.android.ui.orders.wooshippinglabels.customs.ShouldRequireCustomsForm
import com.woocommerce.android.ui.orders.wooshippinglabels.models.OriginShippingAddress
Expand Down Expand Up @@ -215,6 +216,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() {
private val getShippingRates: GetShippingRates = mock()
private val purchaseShippingLabel: PurchaseShippingLabel = mock()
private val observeStoreOptions: ObserveStoreOptions = mock()
private val verifyDestinationAddress: VerifyDestinationAddress = mock()

private lateinit var sut: WooShippingLabelCreationViewModel

Expand All @@ -230,6 +232,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() {
observeStoreOptions = observeStoreOptions,
fetchAccountSettings = mock(),
shouldRequireCustoms = shouldRequireCustomsForm,
verifyDestinationAddress = verifyDestinationAddress,
savedState = savedState
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.woocommerce.android.ui.orders.wooshippinglabels.address.destination

import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.orders.wooshippinglabels.models.DestinationShippingAddress
import com.woocommerce.android.ui.orders.wooshippinglabels.networking.WooShippingLabelRepository
import com.woocommerce.android.viewmodel.BaseUnitTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.kotlin.any
import org.mockito.kotlin.whenever
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType.UNKNOWN
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooError
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType.GENERIC_ERROR
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult

@OptIn(ExperimentalCoroutinesApi::class)
class VerifyDestinationAddressTests : BaseUnitTest() {
private val repository: WooShippingLabelRepository = mock()
private val site: SelectedSite = mock()

private val sut = VerifyDestinationAddress(repository, site, coroutinesTestRule.testDispatchers)

private val defaultOrderId = 2L
private val defaultAddressResponse = DestinationShippingAddress.EMPTY

@Test
fun `when selected site is null then return failure`() = testBlocking {
val result = sut.invoke(defaultOrderId)
assert(result.isFailure)
}

@Test
fun `when verify address fails then return failure`() = testBlocking {
whenever(site.getOrNull()).thenReturn(SiteModel())
whenever(repository.verifyDestinationAddress(any(), any()))
.thenReturn(WooResult(WooError(GENERIC_ERROR, UNKNOWN)))

val result = sut.invoke(defaultOrderId)

assert(result.isFailure)
}

@Test
fun `when verify address succeed then return expected data`() = testBlocking {
whenever(site.getOrNull()).thenReturn(SiteModel())
whenever(repository.verifyDestinationAddress(any(), any()))
.thenReturn(WooResult(defaultAddressResponse))

val result = sut.invoke(defaultOrderId)

assert(result.isSuccess)
assertThat(result.getOrNull()).isEqualTo(defaultAddressResponse)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,17 @@ class ShouldRequireCustomsFormTest {
)
assertFalse(shouldRequireCustomsForm(addressData))
}

@Test
fun `should return false for empty destination addresses`() {
val addressData = WooShippingAddresses(
shipFrom = OriginShippingAddress.EMPTY.copy(country = "US", state = "CA"),
shipTo = DestinationShippingAddress(
Address.EMPTY,
false
),
originAddresses = emptyList()
)
assertFalse(shouldRequireCustomsForm(addressData))
}
}

0 comments on commit 01c6d89

Please sign in to comment.