diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/BookingsRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/BookingsRepository.kt index 430e472e36a..c9ce7f699a3 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/BookingsRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/BookingsRepository.kt @@ -24,7 +24,7 @@ class BookingsRepository @Inject constructor( page: Int, perPage: Int, query: String? = null, - filters: BookingFilters? = null, + filters: BookingFilters = BookingFilters.EMPTY, order: BookingsOrderOption ): Result { val result = bookingsStore.fetchBookings( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/list/BookingListFiltersBuilder.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/list/BookingListFiltersBuilder.kt index 5cc42cd2c77..0dc71d6ed50 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/list/BookingListFiltersBuilder.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/list/BookingListFiltersBuilder.kt @@ -1,10 +1,8 @@ package com.woocommerce.android.ui.bookings.list +import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsDateRangePresets import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsFilterOption import java.time.Clock -import java.time.LocalDate -import java.time.LocalTime -import java.time.ZoneOffset import javax.inject.Inject class BookingListFiltersBuilder @Inject constructor( @@ -12,27 +10,10 @@ class BookingListFiltersBuilder @Inject constructor( ) { /** * Returns a [BookingsFilterOption.DateRange] based on the selected [BookingListTab]. - * - * We use UTC for the API calls, as the API stores the dates without timezone information, which means that - * when comparing dates, they are treated as UTC times. - * See p1759398245019489-slack-C09FHQNQERG */ - fun BookingListTab.asDateRangeFilter(): BookingsFilterOption.DateRange? { - fun todayAtMidnight() = LocalDate.now(clock).atTime(LocalTime.MIDNIGHT).atOffset(ZoneOffset.UTC).toInstant() - fun todayAtEndOfDay() = LocalDate.now(clock).atTime(LocalTime.MAX).atOffset(ZoneOffset.UTC).toInstant() - - return when (this) { - BookingListTab.Today -> BookingsFilterOption.DateRange( - before = todayAtEndOfDay(), - after = todayAtMidnight() - ) - - BookingListTab.Upcoming -> BookingsFilterOption.DateRange( - before = null, - after = todayAtEndOfDay() - ) - - BookingListTab.All -> null - } + fun BookingListTab.asDateRangeFilter(): BookingsFilterOption.DateRange? = when (this) { + BookingListTab.Today -> BookingsDateRangePresets.today(clock) + BookingListTab.Upcoming -> BookingsDateRangePresets.upcoming(clock) + BookingListTab.All -> null } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/list/BookingListHandler.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/list/BookingListHandler.kt index fd8c92176f3..00c80631828 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/list/BookingListHandler.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/list/BookingListHandler.kt @@ -32,7 +32,7 @@ class BookingListHandler @Inject constructor( private val canLoadMore = AtomicBoolean(false) private val searchQuery = MutableStateFlow(null) - private val filters = MutableStateFlow(null) + private val filters = MutableStateFlow(BookingFilters.EMPTY) private val sortBy = MutableStateFlow(BookingListSortOption.NewestToOldest) private val searchResults = MutableStateFlow(emptyList()) @@ -57,7 +57,7 @@ class BookingListHandler @Inject constructor( suspend fun loadBookings( searchQuery: String? = null, - filters: BookingFilters? = null, + filters: BookingFilters = BookingFilters.EMPTY, sortBy: BookingListSortOption ): Result = mutex.withLock { // Reset pagination attributes diff --git a/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsDateRangePresets.kt b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsDateRangePresets.kt new file mode 100644 index 00000000000..cff43d4ca28 --- /dev/null +++ b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsDateRangePresets.kt @@ -0,0 +1,47 @@ +package org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings + +import java.time.Clock +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneOffset + +/** + * Factory for canonical booking date range presets used across app and data layers. + * + * Notes about time zone: + * The WC Bookings API stores dates without timezone information. To ensure + * consistent comparisons and filtering on both client and server, we construct + * ranges in UTC. + * See p1759398245019489-slack-C09FHQNQERG + */ +object BookingsDateRangePresets { + /** + * Returns a DateRange that spans the current day in UTC, inclusive of the + * full day when interpreted by the API (midnight to end-of-day). + */ + fun today(clock: Clock): BookingsFilterOption.DateRange { + val start = LocalDate.now(clock) + .atTime(LocalTime.MIDNIGHT) + .atOffset(ZoneOffset.UTC) + .toInstant() + val end = getEndOfToday(clock) + return BookingsFilterOption.DateRange(before = end, after = start) + } + + /** + * Returns a DateRange that includes bookings strictly after the end of the + * current day in UTC (i.e., upcoming bookings from tomorrow onwards). + */ + fun upcoming(clock: Clock): BookingsFilterOption.DateRange { + val endOfToday = getEndOfToday(clock) + return BookingsFilterOption.DateRange(before = null, after = endOfToday) + } + + private fun getEndOfToday(clock: Clock): Instant { + return LocalDate.now(clock) + .atTime(LocalTime.MAX) + .atOffset(ZoneOffset.UTC) + .toInstant() + } +} diff --git a/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsStore.kt b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsStore.kt index a77fa849358..01ba0f05956 100644 --- a/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsStore.kt +++ b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsStore.kt @@ -15,6 +15,7 @@ import org.wordpress.android.fluxc.store.WCOrderStore import org.wordpress.android.fluxc.tools.CoroutineEngine import org.wordpress.android.fluxc.utils.HeadersParser import org.wordpress.android.util.AppLog +import java.time.Clock import javax.inject.Inject import javax.inject.Singleton @@ -26,13 +27,14 @@ class BookingsStore @Inject internal constructor( private val bookingDtoMapper: BookingDtoMapper, private val headersParser: HeadersParser, private val coroutineEngine: CoroutineEngine, + private val clock: Clock, ) { suspend fun fetchBookings( site: SiteModel, perPage: Int = BookingsRestClient.DEFAULT_PER_PAGE, page: Int = 1, query: String? = null, - filters: BookingFilters?, + filters: BookingFilters, order: BookingsOrderOption ): WooResult { return coroutineEngine.withDefaultContext(AppLog.T.API, this, "fetchBookings") { @@ -54,9 +56,28 @@ class BookingsStore @Inject internal constructor( ) } } - if (page == 1 && filters == BookingFilters.EMPTY && query.isNullOrEmpty()) { - // Clear existing bookings and insert new ones when fetching the first page - bookingsDao.replaceAllForSite(site.localId(), entities) + // Clear existing bookings when fetching the first page. + // If filters are applied, only clear entries that match the applied filters, + // otherwise (no filters) clear all entries for the site. + if (page == 1 && query.isNullOrEmpty()) { + when { + filters == BookingFilters.EMPTY -> { + bookingsDao.replaceAllForSite(site.localId(), entities) + } + + filters.dateRange != null && filters.isTodayOrUpcoming -> { + bookingsDao.cleanAndUpsertBookings( + site.localId(), + filters.dateRange, + entities + ) + } + + else -> { + // For any other filters, avoid deletions to prevent removing unrelated cached items + bookingsDao.insertOrReplace(entities) + } + } } else { bookingsDao.insertOrReplace(entities) } @@ -77,6 +98,13 @@ class BookingsStore @Inject internal constructor( } } + private val BookingFilters.isTodayOrUpcoming: Boolean + get() { + val today = BookingsDateRangePresets.today(clock) + val upcoming = BookingsDateRangePresets.upcoming(clock) + return (dateRange == today || dateRange == upcoming) && enabledFiltersCount == 1 + } + fun observeBookings( site: SiteModel, limit: Int? = null, diff --git a/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/persistence/dao/BookingsDao.kt b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/persistence/dao/BookingsDao.kt index 25510451d87..0bd1497b97e 100644 --- a/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/persistence/dao/BookingsDao.kt +++ b/libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/persistence/dao/BookingsDao.kt @@ -8,6 +8,7 @@ import androidx.room.Transaction import kotlinx.coroutines.flow.Flow import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingFilters +import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsFilterOption import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsOrderOption import org.wordpress.android.fluxc.persistence.entity.BookingEntity import org.wordpress.android.fluxc.persistence.entity.BookingResourceEntity @@ -70,6 +71,55 @@ interface BookingsDao { insertOrReplace(entities) } + @Suppress("LongParameterList") + @Query( + """ + DELETE FROM Bookings + WHERE localSiteId = :localSiteId + AND (:startDateBefore IS NULL OR start <= :startDateBefore) + AND (:startDateAfter IS NULL OR start >= :startDateAfter) + AND ((:idsSize = 0) OR id NOT IN (:ids)) + """ + ) + suspend fun deleteForSiteWithDateRangeFilter( + localSiteId: LocalId, + startDateBefore: Long?, + startDateAfter: Long?, + ids: List, + idsSize: Int, + ) + + private suspend fun deleteForSiteWithDateRangeFilter( + localSiteId: LocalId, + dateRange: BookingsFilterOption.DateRange, + ids: List + ) { + deleteForSiteWithDateRangeFilter( + localSiteId = localSiteId, + startDateBefore = dateRange.before?.epochSecond, + startDateAfter = dateRange.after?.epochSecond, + ids = ids, + idsSize = ids.size, + ) + } + + /** + * Delete Booking entities that are not present in the new list and then insert the new entities + */ + @Transaction + suspend fun cleanAndUpsertBookings( + localSiteId: LocalId, + dateRange: BookingsFilterOption.DateRange, + entities: List, + ) { + deleteForSiteWithDateRangeFilter( + localSiteId = localSiteId, + dateRange = dateRange, + ids = entities.map { it.id.value }, + ) + insertOrReplace(entities) + } + fun observeBookings( localSiteId: LocalId, limit: Int? = null, diff --git a/libs/fluxc-plugin/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsStoreTest.kt b/libs/fluxc-plugin/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsStoreTest.kt index bf35d29d0dd..546f920ca2e 100644 --- a/libs/fluxc-plugin/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsStoreTest.kt +++ b/libs/fluxc-plugin/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/bookings/BookingsStoreTest.kt @@ -26,6 +26,7 @@ import org.wordpress.android.fluxc.persistence.entity.OrderEntity import org.wordpress.android.fluxc.store.WCOrderStore import org.wordpress.android.fluxc.tools.CoroutineEngine import org.wordpress.android.fluxc.utils.HeadersParser +import java.time.Clock import java.time.Instant import kotlin.coroutines.EmptyCoroutineContext @@ -38,6 +39,7 @@ class BookingsStoreTest { private val bookingsDao: BookingsDao = mock() private val bookingDtoMapper: BookingDtoMapper = BookingDtoMapper(mock()) private val headersParser: HeadersParser = mock() + private val clock: Clock = Clock.systemUTC() @Before fun setUp() { @@ -47,63 +49,66 @@ class BookingsStoreTest { bookingsDao = bookingsDao, bookingDtoMapper = bookingDtoMapper, headersParser = headersParser, - coroutineEngine = CoroutineEngine(EmptyCoroutineContext, mock()) + coroutineEngine = CoroutineEngine(EmptyCoroutineContext, mock()), + clock = clock, ) } @Test - fun `given refreshOrder is false, when updateBooking succeeds, then inserts mapped entity and returns it`(): Unit = runBlocking { - // given - val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } - val dto = sampleBookingDto() - val storedBooking = sampleBookingEntity(order = BookingOrderInfo(productInfo = null)) - whenever(bookingsDao.getBooking(TEST_LOCAL_SITE_ID, dto.id)).thenReturn(storedBooking) - whenever(bookingsRestClient.updateBooking(site, dto.id, BookingUpdatePayload(note = "n"))) - .thenReturn(WooPayload(dto)) - whenever(bookingsDao.insertOrReplace(any())).thenReturn(1L) + fun `given refreshOrder is false, when updateBooking succeeds, then inserts mapped entity and returns it`(): Unit = + runBlocking { + // given + val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } + val dto = sampleBookingDto() + val storedBooking = sampleBookingEntity(order = BookingOrderInfo(productInfo = null)) + whenever(bookingsDao.getBooking(TEST_LOCAL_SITE_ID, dto.id)).thenReturn(storedBooking) + whenever(bookingsRestClient.updateBooking(site, dto.id, BookingUpdatePayload(note = "n"))) + .thenReturn(WooPayload(dto)) + whenever(bookingsDao.insertOrReplace(any())).thenReturn(1L) - // when - val result = sut.updateBooking( - site = site, - bookingId = dto.id, - bookingUpdatePayload = BookingUpdatePayload(note = "n"), - refreshOrder = false - ) + // when + val result = sut.updateBooking( + site = site, + bookingId = dto.id, + bookingUpdatePayload = BookingUpdatePayload(note = "n"), + refreshOrder = false + ) - // then - assertThat(result.isError).isFalse() - assertThat(result.model).isNotNull - // The store preserves the stored order on the mapped entity - verify(bookingsDao).insertOrReplace(argThat { this.order == storedBooking.order }) - } + // then + assertThat(result.isError).isFalse() + assertThat(result.model).isNotNull + // The store preserves the stored order on the mapped entity + verify(bookingsDao).insertOrReplace(argThat { this.order == storedBooking.order }) + } @Test - fun `given refreshOrder is true, when updateBooking succeeds, then refreshes order and inserts mapped entity`(): Unit = runBlocking { - // given - val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } - val dto = sampleBookingDto() + fun `given refreshOrder is true, when updateBooking succeeds, then refreshes order and inserts mapped entity`(): Unit = + runBlocking { + // given + val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } + val dto = sampleBookingDto() - val fetchedOrder = OrderEntity(orderId = dto.orderId, localSiteId = TEST_LOCAL_SITE_ID) - whenever(orderStore.fetchSingleOrderSync(site, dto.orderId)).thenReturn(WooResult(fetchedOrder)) - whenever(bookingsRestClient.updateBooking(site, dto.id, BookingUpdatePayload(status = Status.Confirmed))) - .thenReturn(WooPayload(dto)) - whenever(bookingsDao.insertOrReplace(any())).thenReturn(1L) + val fetchedOrder = OrderEntity(orderId = dto.orderId, localSiteId = TEST_LOCAL_SITE_ID) + whenever(orderStore.fetchSingleOrderSync(site, dto.orderId)).thenReturn(WooResult(fetchedOrder)) + whenever(bookingsRestClient.updateBooking(site, dto.id, BookingUpdatePayload(status = Status.Confirmed))) + .thenReturn(WooPayload(dto)) + whenever(bookingsDao.insertOrReplace(any())).thenReturn(1L) - // when - val result = sut.updateBooking( - site = site, - bookingId = dto.id, - bookingUpdatePayload = BookingUpdatePayload(status = Status.Confirmed), - refreshOrder = true - ) + // when + val result = sut.updateBooking( + site = site, + bookingId = dto.id, + bookingUpdatePayload = BookingUpdatePayload(status = Status.Confirmed), + refreshOrder = true + ) - // then - assertThat(result.isError).isFalse() - val expected = with(bookingDtoMapper) { dto.toEntity(TEST_LOCAL_SITE_ID, fetchedOrder) } - assertThat(result.model).isEqualTo(expected) - verify(bookingsDao).insertOrReplace(expected) - verify(bookingsDao, never()).getBooking(TEST_LOCAL_SITE_ID, dto.id) - } + // then + assertThat(result.isError).isFalse() + val expected = with(bookingDtoMapper) { dto.toEntity(TEST_LOCAL_SITE_ID, fetchedOrder) } + assertThat(result.model).isEqualTo(expected) + verify(bookingsDao).insertOrReplace(expected) + verify(bookingsDao, never()).getBooking(TEST_LOCAL_SITE_ID, dto.id) + } @Test fun `given rest client fails, when updateBooking, then returns error and does not insert`(): Unit = runBlocking { @@ -169,6 +174,257 @@ class BookingsStoreTest { verify(bookingsDao).insertOrReplace(argThat { this.order == storedBooking.order }) } + @Test + fun `given page 1 and today date filter and no query, when fetchBookings, then delete matching and insert is called`(): Unit = + runBlocking { + // given + val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } + val dto = sampleBookingDto().copy(orderId = 0L) // avoid order fetch + val filters = BookingFilters( + dateRange = BookingsDateRangePresets.today(clock) + ) + whenever( + bookingsRestClient.fetchBookings( + site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + ).thenReturn(WooPayload(arrayOf(dto))) + whenever(headersParser.getTotalPages(any())).thenReturn(null) + + // when + val result = sut.fetchBookings( + site = site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + + // then + assertThat(result.isError).isFalse() + // We delete only matching filtered rows then insert the fetched page + verify(bookingsDao).cleanAndUpsertBookings( + any(), + any(), + any>() + ) + verify(bookingsDao, never()).replaceAllForSite(any(), any()) + } + + @Test + fun `given page 1, today date filter, customer filter and no query, when fetchBookings, then only insertOrReplace is called`(): Unit = + runBlocking { + // given + val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } + val dto = sampleBookingDto().copy(orderId = 0L) // avoid order fetch + val filters = BookingFilters( + dateRange = BookingsDateRangePresets.today(clock), + customer = BookingsFilterOption.Customer(customerId = 1L, customerName = "name"), + ) + whenever( + bookingsRestClient.fetchBookings( + site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + ).thenReturn(WooPayload(arrayOf(dto))) + whenever(headersParser.getTotalPages(any())).thenReturn(null) + + // when + val result = sut.fetchBookings( + site = site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + + // then + assertThat(result.isError).isFalse() + // We delete only matching filtered rows then insert the fetched page + verify(bookingsDao).insertOrReplace(any>()) + verify(bookingsDao, never()).cleanAndUpsertBookings( + any(), + any(), + any>() + ) + verify(bookingsDao, never()).replaceAllForSite(any(), any()) + } + + @Test + fun `given page 1 and upcoming date filter and no query, when fetchBookings, then delete matching and insert is called`(): Unit = + runBlocking { + // given + val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } + val dto = sampleBookingDto().copy(orderId = 0L) // avoid order fetch + val filters = BookingFilters( + dateRange = BookingsDateRangePresets.upcoming(clock) + ) + whenever( + bookingsRestClient.fetchBookings( + site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + ).thenReturn(WooPayload(arrayOf(dto))) + whenever(headersParser.getTotalPages(any())).thenReturn(null) + + // when + val result = sut.fetchBookings( + site = site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + + // then + assertThat(result.isError).isFalse() + // We delete only matching filtered rows then insert the fetched page + verify(bookingsDao).cleanAndUpsertBookings( + any(), + any(), + any>() + ) + verify(bookingsDao, never()).replaceAllForSite(any(), any()) + } + + @Test + fun `given page greater than 1 with multiple filters, when fetchBookings, then insertOrReplace is called`(): Unit = + runBlocking { + // given + val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } + val dto = sampleBookingDto().copy(orderId = 0L) + val filters = BookingFilters( + dateRange = BookingsFilterOption.DateRange(before = Instant.now(), after = Instant.now()), + customer = BookingsFilterOption.Customer(customerId = 1L, customerName = "name") + ) + whenever( + bookingsRestClient.fetchBookings( + site, + perPage = 25, + page = 2, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + ) + .thenReturn(WooPayload(arrayOf(dto))) + whenever(headersParser.getTotalPages(any())).thenReturn(null) + + // when + val result = sut.fetchBookings( + site = site, + perPage = 25, + page = 2, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + + // then + assertThat(result.isError).isFalse() + verify(bookingsDao).insertOrReplace(any>()) + verify(bookingsDao, never()).replaceAllForSite(any(), any()) + verify(bookingsDao, never()) + .cleanAndUpsertBookings(any(), any(), any>()) + } + + @Test + fun `given page 1 with no filters and no query, when fetchBookings, then replaceAllForSite is called`(): Unit = + runBlocking { + // given + val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } + val dto = sampleBookingDto().copy(orderId = 0L) + val filters: BookingFilters = BookingFilters.EMPTY + whenever( + bookingsRestClient.fetchBookings( + site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + ).thenReturn(WooPayload(arrayOf(dto))) + whenever(headersParser.getTotalPages(any())).thenReturn(null) + + // when + val result = sut.fetchBookings( + site = site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + + // then + assertThat(result.isError).isFalse() + verify(bookingsDao).replaceAllForSite(any(), any()) + verify(bookingsDao, never()).cleanAndUpsertBookings( + any(), + any(), + any>() + ) + } + + @Test + fun `given page 1 with custom date range filter and no query, when fetchBookings, then only insertOrReplace is called`(): Unit = + runBlocking { + // given + val site = SiteModel().apply { id = TEST_LOCAL_SITE_ID.value } + val dto = sampleBookingDto().copy(orderId = 0L) + val filters = BookingFilters( + dateRange = BookingsFilterOption.DateRange(before = Instant.now(), after = Instant.now()), + customer = BookingsFilterOption.Customer(customerId = 1L, customerName = "name"), + ) + whenever( + bookingsRestClient.fetchBookings( + site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + ).thenReturn(WooPayload(arrayOf(dto))) + whenever(headersParser.getTotalPages(any())).thenReturn(null) + + // when + val result = sut.fetchBookings( + site = site, + perPage = 25, + page = 1, + query = null, + filters = filters, + order = BookingsOrderOption.DESC + ) + + // then + assertThat(result.isError).isFalse() + verify(bookingsDao).insertOrReplace(any>()) + verify(bookingsDao, never()).replaceAllForSite(any(), any()) + verify(bookingsDao, never()).cleanAndUpsertBookings( + any(), + any(), + any>() + ) + } + private fun sampleBookingDto(): BookingDto = BookingDto( id = TEST_BOOKING_ID, start = Instant.now().epochSecond,