Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e1aab9d
First approach to cache
toupper Aug 29, 2025
3f9d131
Implement cache and use it in view model
toupper Aug 29, 2025
a77834d
Remove comments
toupper Aug 29, 2025
659ad10
Show the cached content right away
toupper Aug 29, 2025
4a5f936
Clear cache when leaving POS
toupper Aug 29, 2025
2bc7118
Add max size for orders cache
toupper Aug 29, 2025
0a349b7
Add tests for cache
toupper Aug 29, 2025
7f2acde
Bring data source back to the orders package
toupper Aug 29, 2025
fe1c4b5
WIP: Merge branch 'feat/WOOMOB-1145-pos-historical-orders-fetching' i…
toupper Sep 2, 2025
d4d8169
Merge branch 'feat/WOOMOB-1145-pos-historical-orders-fetching' into f…
toupper Sep 2, 2025
2539408
Merge branch 'feat/WOOMOB-1145-pos-historical-orders-fetching' into f…
toupper Sep 4, 2025
a8ef33d
Merge branch 'feat/WOOMOB-1145-pos-historical-orders-fetching' into f…
toupper Sep 4, 2025
96c3a09
Fix test
toupper Sep 4, 2025
a57f5d7
Remove non necessary suspend
toupper Sep 4, 2025
333effb
Move file to its right package
toupper Sep 4, 2025
60d1cf9
Fix detekt
toupper Sep 4, 2025
69cc4dd
fix lint and simplify test
toupper Sep 5, 2025
e0e5325
Improve POS Orders cache
toupper Sep 5, 2025
0cadb0c
Remove non necessary code
toupper Sep 5, 2025
fe798f6
Move file to its right package
toupper Sep 5, 2025
10c135c
Revert change
toupper Sep 5, 2025
eab3bc7
Add unit test for clearing cache on splash screen
toupper Sep 5, 2025
1d91751
Add test
toupper Sep 5, 2025
02fa76f
Simplify cache
toupper Sep 5, 2025
a57941c
Make cache a singleton
toupper Sep 5, 2025
9720e3c
Fallback string not necessary
toupper Sep 5, 2025
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 @@ -3,6 +3,8 @@ package com.woocommerce.android.ui.woopos.orders
import com.woocommerce.android.model.Order
import com.woocommerce.android.model.OrderMapper
import com.woocommerce.android.tools.SelectedSite
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
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.persistence.entity.OrderEntity
Expand All @@ -17,26 +19,35 @@ class WooPosOrdersDataSource @Inject constructor(
private val restClient: OrderRestClient,
private val selectedSite: SelectedSite,
private val orderMapper: OrderMapper,
private val ordersCache: WooPosOrdersInMemoryCache
) {
suspend fun loadOrders(): LoadOrdersResult {
companion object {
const val POS_ORDERS_PAGE_SIZE = 25
}
fun loadOrders(): Flow<LoadOrdersResult> = flow {
val cached = ordersCache.getAll()
emit(LoadOrdersResult.Success(cached))

val result = restClient.fetchOrders(
site = selectedSite.get(),
count = 25,
count = POS_ORDERS_PAGE_SIZE,
page = 1,
orderBy = OrderBy.DATE,
sortOrder = OrderRestClient.SortOrder.DESCENDING,
statusFilter = null,
createdVia = "pos-rest-api"
)

return if (result.isError) {
LoadOrdersResult.Error(result.error.message)
if (result.isError) {
emit(LoadOrdersResult.Error(result.error.message))
} else {
LoadOrdersResult.Success(result.orders.toAppModels())
val mapped = result.orders.toAppModels()
ordersCache.setAll(mapped)
emit(LoadOrdersResult.Success(result.orders.toAppModels()))
}
}

private suspend fun List<OrderEntity>.toAppModels(): List<Order> = map {
orderMapper.toAppModel(it)
}
} ?: emptyList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.woocommerce.android.ui.woopos.orders

import com.woocommerce.android.model.Order
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class WooPosOrdersInMemoryCache @Inject constructor() {
private val ordersCache = AtomicReference<List<Order>>(emptyList())

fun setAll(orders: List<Order>) {
ordersCache.set(orders.toList())
}

fun getAll(): List<Order> = ordersCache.get()

fun clear() {
ordersCache.set(emptyList())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,28 @@ class WooPosOrdersViewModel @Inject constructor(
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }

when (val result = ordersDataSource.loadOrders()) {
is LoadOrdersResult.Error -> {
_state.update {
it.copy(
isLoading = false,
error = result.message ?: "Unknown error"
)
ordersDataSource.loadOrders().collect { result ->
when (result) {
is LoadOrdersResult.Error -> {
_state.update {
it.copy(
isLoading = false,
error = result.message
)
}
}
}
is LoadOrdersResult.Success -> {
val list = result.orders
_state.update { prev ->
prev.copy(
isLoading = false,
orders = list,
selectedOrderId = prev.selectedOrderId?.takeIf { id ->
list.any { o -> o.id == id }
} ?: list.firstOrNull()?.id
)

is LoadOrdersResult.Success -> {
val list = result.orders
_state.update { prev ->
prev.copy(
isLoading = false,
orders = list,
selectedOrderId = prev.selectedOrderId?.takeIf { id ->
list.any { o -> o.id == id }
} ?: list.firstOrNull()?.id
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.woocommerce.android.ui.woopos.common.data.WooPosPopularProductsProvider
import com.woocommerce.android.ui.woopos.home.items.products.WooPosProductsDataSource
import com.woocommerce.android.ui.woopos.orders.WooPosOrdersInMemoryCache
import com.woocommerce.android.ui.woopos.tab.WooPosCanBeLaunchedInTab
import com.woocommerce.android.ui.woopos.tab.WooPosLaunchability
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.Loaded
Expand All @@ -22,6 +23,7 @@ class WooPosSplashViewModel @Inject constructor(
private val popularProductsProvider: WooPosPopularProductsProvider,
private val analyticsTracker: WooPosAnalyticsTracker,
private val posCanBeLaunchedInTab: WooPosCanBeLaunchedInTab,
private val ordersCache: WooPosOrdersInMemoryCache
) : ViewModel() {
private val _state = MutableStateFlow<WooPosSplashState>(WooPosSplashState.Loading)
val state: StateFlow<WooPosSplashState> = _state
Expand All @@ -38,7 +40,8 @@ class WooPosSplashViewModel @Inject constructor(

joinAll(
launch { productsDataSource.prepopulateProductsCache() },
launch { popularProductsProvider.fetchAndCachePopularProducts() }
launch { popularProductsProvider.fetchAndCachePopularProducts() },
launch { ordersCache.clear() }
)
_state.value = WooPosSplashState.Loaded
trackPosLoaded(splashScreenStartTime)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.woocommerce.android.ui.woopos.common.data

import com.woocommerce.android.ui.orders.OrderTestUtils
import com.woocommerce.android.ui.woopos.orders.WooPosOrdersInMemoryCache
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class WooPosOrdersInMemoryCacheTest {

private lateinit var cache: WooPosOrdersInMemoryCache

@Before
fun setup() {
cache = WooPosOrdersInMemoryCache()
}

@Test
fun `when cache is empty, then getAll returns empty list`() {
// WHEN
val result = cache.getAll()

// THEN
assertThat(result).isEmpty()
}

@Test
fun `when setAll is called, then getAll returns the same elements`() {
// GIVEN
val orders = listOf(
OrderTestUtils.generateTestOrder(1),
OrderTestUtils.generateTestOrder(2)
)

// WHEN
cache.setAll(orders)
val result = cache.getAll()

// THEN
assertThat(result).containsExactlyElementsOf(orders)
}

@Test
fun `when setAll is called twice, then last write wins`() {
// GIVEN
val first = listOf(
OrderTestUtils.generateTestOrder(1),
OrderTestUtils.generateTestOrder(2)
)
val second = listOf(
OrderTestUtils.generateTestOrder(3),
OrderTestUtils.generateTestOrder(4),
OrderTestUtils.generateTestOrder(5)
)

// WHEN
cache.setAll(first)
cache.setAll(second)
val result = cache.getAll()

// THEN
assertThat(result).containsExactlyElementsOf(second)
}

@Test
fun `when cache is cleared, then getAll returns empty list`() {
// GIVEN
val orders = listOf(
OrderTestUtils.generateTestOrder(1),
OrderTestUtils.generateTestOrder(2)
)
cache.setAll(orders)

// WHEN
cache.clear()
val result = cache.getAll()

// THEN
assertThat(result).isEmpty()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.orders.OrderTestUtils
import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.eq
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.LocalOrRemoteId
Expand All @@ -33,25 +35,35 @@ class WooPosOrdersDataSourceTest {
private val siteModel: SiteModel = mock()
private val selectedSite: SelectedSite = mock { on { get() }.thenReturn(siteModel) }
private val orderMapper: OrderMapper = mock()
private val ordersCache: WooPosOrdersInMemoryCache = mock()

private val sut = WooPosOrdersDataSource(
restClient = orderRestClient,
selectedSite = selectedSite,
orderMapper = orderMapper
orderMapper = orderMapper,
ordersCache = ordersCache
)

@Test
fun `given rest client returns entities, when loadOrders called, then should map them to app models`() = runTest {
fun `given cache and successful fetch, when loadOrders collected, then emits cache first then mapped network and stores in cache`() = runTest {
// GIVEN
val cachedOrder = OrderTestUtils.generateTestOrder()
whenever(ordersCache.getAll()).thenReturn(listOf(cachedOrder))

// Network returns two entities
val e1 = OrderEntity(localSiteId = LocalOrRemoteId.LocalId(1), 1)
val e2 = OrderEntity(localSiteId = LocalOrRemoteId.LocalId(1), 2)
val entities = listOf(
e1 to emptyList<WCMetaData>(),
e2 to emptyList<WCMetaData>()
)

val firstOrder = OrderTestUtils.generateTestOrder()
val secondOrder = OrderTestUtils.generateTestOrder()

whenever(orderMapper.toAppModel(e1)).thenReturn(firstOrder)
whenever(orderMapper.toAppModel(e2)).thenReturn(secondOrder)

val payload = WCOrderStore.FetchOrdersResponsePayload(
site = siteModel,
ordersWithMeta = entities
Expand All @@ -69,18 +81,24 @@ class WooPosOrdersDataSourceTest {
)
).thenReturn(payload)

whenever(orderMapper.toAppModel(e1)).thenReturn(firstOrder)
whenever(orderMapper.toAppModel(e2)).thenReturn(secondOrder)

// WHEN
val result = sut.loadOrders()
val emissions = sut.loadOrders().toList(mutableListOf())

// THEN
assertThat(result).isInstanceOf(LoadOrdersResult.Success::class.java)
val success = result as LoadOrdersResult.Success
assertThat(success.orders).containsExactly(firstOrder, secondOrder)
assertThat(emissions).hasSize(2)
// First emission = cache
val first = emissions[0]
assertThat(first).isInstanceOf(LoadOrdersResult.Success::class.java)
assertThat((first as LoadOrdersResult.Success).orders).containsExactly(cachedOrder)

// Second emission = network mapped
val second = emissions[1]
assertThat(second).isInstanceOf(LoadOrdersResult.Success::class.java)
assertThat((second as LoadOrdersResult.Success).orders).containsExactly(firstOrder, secondOrder)

verify(selectedSite).get()
verify(ordersCache).getAll()
verify(ordersCache).setAll(listOf(firstOrder, secondOrder))
verify(orderRestClient).fetchOrders(
site = eq(siteModel),
count = eq(25),
Expand All @@ -93,8 +111,11 @@ class WooPosOrdersDataSourceTest {
}

@Test
fun `given store returns error, when loadOrders called, then should return error result`() = runTest {
fun `given cache and fetch error, when loadOrders collected, then emits cache then error without caching`() = runTest {
// GIVEN
val cachedOrder = OrderTestUtils.generateTestOrder()
whenever(ordersCache.getAll()).thenReturn(listOf(cachedOrder))

val orderError = WCOrderStore.OrderError(
type = WCOrderStore.OrderErrorType.GENERIC_ERROR,
message = "generic error"
Expand All @@ -118,13 +139,21 @@ class WooPosOrdersDataSourceTest {
).thenReturn(payload)

// WHEN
val result = sut.loadOrders()
val emissions = sut.loadOrders().toList(mutableListOf())

// THEN
assertThat(result).isInstanceOf(LoadOrdersResult.Error::class.java)
val error = result as LoadOrdersResult.Error
assertThat(error.message).isEqualTo(orderError.message)
assertThat(emissions).hasSize(2)

val first = emissions[0]
assertThat(first).isInstanceOf(LoadOrdersResult.Success::class.java)
assertThat((first as LoadOrdersResult.Success).orders).containsExactly(cachedOrder)

val second = emissions[1]
assertThat(second).isInstanceOf(LoadOrdersResult.Error::class.java)
assertThat((second as LoadOrdersResult.Error).message).isEqualTo("generic error")

verify(ordersCache).getAll()
verify(ordersCache, never()).setAll(any())
verify(orderRestClient).fetchOrders(
site = eq(siteModel),
count = eq(25),
Expand All @@ -137,13 +166,14 @@ class WooPosOrdersDataSourceTest {
}

@Test
fun `given default site and pagination, when loadOrders called, then should forward params including createdVia`() = runTest {
fun `given empty cache, when loadOrders collected, then forwards params including createdVia and emits empty then empty`() = runTest {
// GIVEN
whenever(ordersCache.getAll()).thenReturn(emptyList())

val payload = WCOrderStore.FetchOrdersResponsePayload(
site = siteModel,
ordersWithMeta = emptyList()
)

whenever(
orderRestClient.fetchOrders(
site = eq(siteModel),
Expand All @@ -157,14 +187,22 @@ class WooPosOrdersDataSourceTest {
).thenReturn(payload)

// WHEN
val result = sut.loadOrders()
val emissions = sut.loadOrders().toList(mutableListOf())

// THEN
assertThat(result).isInstanceOf(LoadOrdersResult.Success::class.java)
val success = result as LoadOrdersResult.Success
assertThat(success.orders).isEmpty()
assertThat(emissions).hasSize(2)

val first = emissions[0]
assertThat(first).isInstanceOf(LoadOrdersResult.Success::class.java)
assertThat((first as LoadOrdersResult.Success).orders).isEmpty()

val second = emissions[1]
assertThat(second).isInstanceOf(LoadOrdersResult.Success::class.java)
assertThat((second as LoadOrdersResult.Success).orders).isEmpty()

verify(selectedSite).get()
verify(ordersCache).getAll()
verify(ordersCache).setAll(emptyList())
verify(orderRestClient).fetchOrders(
site = eq(siteModel),
count = eq(25),
Expand Down
Loading