diff --git a/build.gradle b/build.gradle index 39840c807f..a46f7526f1 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ allprojects { ext { minSdkVersion = 24 - compileSdkVersion = 33 + compileSdkVersion = 34 targetSdkVersion = 33 } diff --git a/example/src/androidTest/java/org/wordpress/android/fluxc/mocked/MockedStack_WCProductsTest.kt b/example/src/androidTest/java/org/wordpress/android/fluxc/mocked/MockedStack_WCProductsTest.kt index d26ee94e91..a75f92cb6c 100644 --- a/example/src/androidTest/java/org/wordpress/android/fluxc/mocked/MockedStack_WCProductsTest.kt +++ b/example/src/androidTest/java/org/wordpress/android/fluxc/mocked/MockedStack_WCProductsTest.kt @@ -505,7 +505,6 @@ class MockedStack_WCProductsTest : MockedStack_Base() { assertEquals(25, payload.reviews.size) assertNull(payload.filterProductIds) assertNull(payload.filterByStatus) - assertFalse(payload.loadedMore) assertTrue(payload.canLoadMore) // Save product reviews to the database diff --git a/example/src/androidTest/java/org/wordpress/android/fluxc/release/ReleaseStack_WCProductTest.kt b/example/src/androidTest/java/org/wordpress/android/fluxc/release/ReleaseStack_WCProductTest.kt index b03886b650..9b14fa0800 100644 --- a/example/src/androidTest/java/org/wordpress/android/fluxc/release/ReleaseStack_WCProductTest.kt +++ b/example/src/androidTest/java/org/wordpress/android/fluxc/release/ReleaseStack_WCProductTest.kt @@ -81,6 +81,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { } @Inject internal lateinit var productStore: WCProductStore + @Inject internal lateinit var mediaStore: MediaStore // must be injected for onMediaListFetched() private var nextEvent: TestEvent = TestEvent.NONE @@ -131,7 +132,12 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { ProductSqlUtils.deleteProductsForSite(sSite) assertEquals(ProductSqlUtils.getProductCountForSite(sSite), 0) - productStore.fetchSingleProduct(FetchSingleProductPayload(sSite, productModel.remoteProductId)) + productStore.fetchSingleProduct( + FetchSingleProductPayload( + sSite, + productModel.remoteProductId + ) + ) // Verify results val fetchedProduct = productStore.getProductByRemoteId(sSite, productModel.remoteProductId) @@ -147,10 +153,16 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { @Test fun testFetchSingleVariation() = runBlocking { // remove all variation for this site and verify there are none - ProductSqlUtils.deleteVariationsForProduct(sSite, productModelWithVariations.remoteProductId) + ProductSqlUtils.deleteVariationsForProduct( + sSite, + productModelWithVariations.remoteProductId + ) assertEquals( - ProductSqlUtils.getVariationsForProduct(sSite, productModelWithVariations.remoteProductId).size, - 0 + ProductSqlUtils.getVariationsForProduct( + sSite, + productModelWithVariations.remoteProductId + ).size, + 0 ) val result = productStore.fetchSingleVariation( @@ -164,16 +176,19 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { // Verify results val fetchedVariation = productStore.getVariationByRemoteId( - sSite, - variationModel.remoteProductId, - variationModel.remoteVariationId + sSite, + variationModel.remoteProductId, + variationModel.remoteVariationId ) assertNotNull(fetchedVariation) assertEquals(fetchedVariation!!.remoteProductId, variationModel.remoteProductId) assertEquals(fetchedVariation.remoteVariationId, variationModel.remoteVariationId) // Verify there's only one variation for this site - assertEquals(1, ProductSqlUtils.getVariationsForProduct(sSite, variationModel.remoteProductId).size) + assertEquals( + 1, + ProductSqlUtils.getVariationsForProduct(sSite, variationModel.remoteProductId).size + ) } @Throws(InterruptedException::class) @@ -186,8 +201,8 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { nextEvent = TestEvent.FETCHED_PRODUCTS mCountDownLatch = CountDownLatch(1) mDispatcher.dispatch( - WCProductActionBuilder - .newFetchProductsAction(FetchProductsPayload(sSite)) + WCProductActionBuilder + .newFetchProductsAction(FetchProductsPayload(sSite)) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) @@ -200,8 +215,16 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { @Test fun testFetchProductVariations() = runBlocking { // remove all variations for this product and verify there are none - ProductSqlUtils.deleteVariationsForProduct(sSite, productModelWithVariations.remoteProductId) - assertEquals(ProductSqlUtils.getVariationsForProduct(sSite, productModelWithVariations.remoteProductId).size, 0) + ProductSqlUtils.deleteVariationsForProduct( + sSite, + productModelWithVariations.remoteProductId + ) + assertEquals( + ProductSqlUtils.getVariationsForProduct( + sSite, + productModelWithVariations.remoteProductId + ).size, 0 + ) productStore.fetchProductVariations( FetchProductVariationsPayload( @@ -211,7 +234,10 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { ) // Verify results - val fetchedVariations = productStore.getVariationsForProduct(sSite, productModelWithVariations.remoteProductId) + val fetchedVariations = productStore.getVariationsForProduct( + sSite, + productModelWithVariations.remoteProductId + ) assertNotEquals(fetchedVariations.size, 0) } @@ -228,9 +254,9 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { nextEvent = TestEvent.FETCHED_PRODUCT_SHIPPING_CLASS_LIST mCountDownLatch = CountDownLatch(1) mDispatcher.dispatch( - WCProductActionBuilder.newFetchProductShippingClassListAction( - FetchProductShippingClassListPayload(sSite) - ) + WCProductActionBuilder.newFetchProductShippingClassListAction( + FetchProductShippingClassListPayload(sSite) + ) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) @@ -253,15 +279,15 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { nextEvent = TestEvent.FETCHED_SINGLE_PRODUCT_SHIPPING_CLASS mCountDownLatch = CountDownLatch(1) mDispatcher.dispatch( - WCProductActionBuilder.newFetchSingleProductShippingClassAction( - FetchSingleProductShippingClassPayload(sSite, remoteShippingClassId) - ) + WCProductActionBuilder.newFetchSingleProductShippingClassAction( + FetchSingleProductShippingClassPayload(sSite, remoteShippingClassId) + ) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) // Verify results val fetchedShippingClasses = productStore.getShippingClassByRemoteId( - sSite, remoteShippingClassId + sSite, remoteShippingClassId ) assertNotNull(fetchedShippingClasses) } @@ -276,7 +302,11 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { nextEvent = TestEvent.FETCH_PRODUCT_CATEGORIES mCountDownLatch = CountDownLatch(1) mDispatcher.dispatch( - WCProductActionBuilder.newFetchProductCategoriesAction(FetchProductCategoriesPayload(sSite)) + WCProductActionBuilder.newFetchProductCategoriesAction( + FetchProductCategoriesPayload( + sSite + ) + ) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) @@ -300,9 +330,9 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { name = "Test" + Random.nextInt(0, 10000) } mDispatcher.dispatch( - WCProductActionBuilder.newAddProductCategoryAction( - AddProductCategoryPayload(sSite, productCategoryModel) - ) + WCProductActionBuilder.newAddProductCategoryAction( + AddProductCategoryPayload(sSite, productCategoryModel) + ) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) @@ -322,7 +352,10 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { productStore.deleteAllProductReviews() assertEquals(0, ProductSqlUtils.getProductReviewsForSite(sSite).size) - productStore.fetchProductReviews(FetchProductReviewsPayload(sSite, offset = 0)) + productStore.fetchProductReviews( + FetchProductReviewsPayload(sSite, offset = 0), + deletePreviouslyCachedReviews = false + ) // Verify results val fetchedReviewsAll = productStore.getProductReviewsForSite(sSite) @@ -338,7 +371,10 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { productStore.deleteAllProductReviews() assertEquals(0, ProductSqlUtils.getProductReviewsForSite(sSite).size) - productStore.fetchProductReviews(FetchProductReviewsPayload(sSite, reviewIds = idsToFetch, offset = 0)) + productStore.fetchProductReviews( + FetchProductReviewsPayload(sSite, reviewIds = idsToFetch, offset = 0), + deletePreviouslyCachedReviews = false + ) // Verify results val fetchReviewsId = productStore.getProductReviewsForSite(sSite) @@ -352,13 +388,25 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { // Check to see how many reviews currently exist for these product IDs before deleting // from the database - val reviewsByProduct = productIdsToFetch.map { productStore.getProductReviewsForProductAndSiteId(sSite.id, it) } + val reviewsByProduct = productIdsToFetch.map { + productStore.getProductReviewsForProductAndSiteId( + sSite.id, + it + ) + } // Remove all product reviews from the database productStore.deleteAllProductReviews() assertEquals(0, ProductSqlUtils.getProductReviewsForSite(sSite).size) - productStore.fetchProductReviews(FetchProductReviewsPayload(sSite, productIds = productIdsToFetch, offset = 0)) + productStore.fetchProductReviews( + FetchProductReviewsPayload( + sSite, + productIds = productIdsToFetch, + offset = 0 + ), + deletePreviouslyCachedReviews = false + ) // Verify results val fetchedReviewsForProduct = productStore.getProductReviewsForSite(sSite) @@ -372,14 +420,14 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { nextEvent = TestEvent.UPDATED_PRODUCT_PASSWORD mCountDownLatch = CountDownLatch(1) mDispatcher.dispatch( - WCProductActionBuilder - .newUpdateProductPasswordAction( - UpdateProductPasswordPayload( - sSite, - productModel.remoteProductId, - updatedPassword - ) - ) + WCProductActionBuilder + .newUpdateProductPasswordAction( + UpdateProductPasswordPayload( + sSite, + productModel.remoteProductId, + updatedPassword + ) + ) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) @@ -387,8 +435,13 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { nextEvent = TestEvent.FETCHED_PRODUCT_PASSWORD mCountDownLatch = CountDownLatch(1) mDispatcher.dispatch( - WCProductActionBuilder - .newFetchProductPasswordAction(FetchProductPasswordPayload(sSite, productModel.remoteProductId)) + WCProductActionBuilder + .newFetchProductPasswordAction( + FetchProductPasswordPayload( + sSite, + productModel.remoteProductId + ) + ) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) } @@ -400,7 +453,12 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { productStore.deleteAllProductReviews() assertEquals(0, ProductSqlUtils.getProductReviewsForSite(sSite).size) - productStore.fetchSingleProductReview(FetchSingleProductReviewPayload(sSite, remoteProductReviewId)) + productStore.fetchSingleProductReview( + FetchSingleProductReviewPayload( + sSite, + remoteProductReviewId + ) + ) // Verify results val review = productStore.getProductReviewByRemoteId(sSite.id, remoteProductReviewId) @@ -413,7 +471,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { // Verify results - review should be deleted from db val savedReview = productStore - .getProductReviewByRemoteId(sSite.id, remoteProductReviewId) + .getProductReviewByRemoteId(sSite.id, remoteProductReviewId) assertNull(savedReview) } @@ -425,7 +483,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { // Verify results val savedReview = productStore - .getProductReviewByRemoteId(sSite.id, remoteProductReviewId) + .getProductReviewByRemoteId(sSite.id, remoteProductReviewId) assertNotNull(savedReview) assertEquals(newStatus, savedReview!!.status) } @@ -437,7 +495,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { // Verify results - review should be deleted from db val savedReview = productStore - .getProductReviewByRemoteId(sSite.id, remoteProductReviewId) + .getProductReviewByRemoteId(sSite.id, remoteProductReviewId) assertNull(savedReview) } @@ -448,7 +506,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { // Verify results val savedReview = productStore - .getProductReviewByRemoteId(sSite.id, remoteProductReviewId) + .getProductReviewByRemoteId(sSite.id, remoteProductReviewId) assertNotNull(savedReview) assertEquals(newStatus, savedReview!!.status) } @@ -474,9 +532,9 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { it.add(WCProductImageModel.fromMediaModel(mediaModelForProduct)) } mDispatcher.dispatch( - WCProductActionBuilder.newUpdateProductImagesAction( - UpdateProductImagesPayload(sSite, productModel.remoteProductId, imageList) - ) + WCProductActionBuilder.newUpdateProductImagesAction( + UpdateProductImagesPayload(sSite, productModel.remoteProductId, imageList) + ) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) @@ -539,7 +597,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { nextEvent = TestEvent.UPDATED_PRODUCT mCountDownLatch = CountDownLatch(1) mDispatcher.dispatch( - WCProductActionBuilder.newUpdateProductAction(UpdateProductPayload(sSite, productModel)) + WCProductActionBuilder.newUpdateProductAction(UpdateProductPayload(sSite, productModel)) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) @@ -591,7 +649,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { nextEvent = TestEvent.UPDATED_PRODUCT mCountDownLatch = CountDownLatch(1) mDispatcher.dispatch( - WCProductActionBuilder.newUpdateProductAction(UpdateProductPayload(sSite, productModel)) + WCProductActionBuilder.newUpdateProductAction(UpdateProductPayload(sSite, productModel)) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) @@ -626,9 +684,9 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { } val updatedVariation = productStore.getVariationByRemoteId( - sSite, - variationModel.remoteProductId, - variationModel.remoteVariationId + sSite, + variationModel.remoteProductId, + variationModel.remoteVariationId ) assertNotNull(updatedVariation) assertEquals(variationModel.remoteProductId, updatedVariation?.remoteProductId) @@ -647,7 +705,11 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { nextEvent = TestEvent.FETCHED_PRODUCT_TAGS mCountDownLatch = CountDownLatch(1) - mDispatcher.dispatch(WCProductActionBuilder.newFetchProductTagsAction(FetchProductTagsPayload(sSite))) + mDispatcher.dispatch( + WCProductActionBuilder.newFetchProductTagsAction( + FetchProductTagsPayload(sSite) + ) + ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) // Verify results @@ -667,9 +729,9 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { val productTags = listOf("Test" + Date().time, "Test1" + Date().time) mDispatcher.dispatch( - WCProductActionBuilder.newAddProductTagsAction( - AddProductTagsPayload(sSite, productTags) - ) + WCProductActionBuilder.newAddProductTagsAction( + AddProductTagsPayload(sSite, productTags) + ) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) @@ -707,7 +769,12 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { mCountDownLatch = CountDownLatch(1) mDispatcher.dispatch( - WCProductActionBuilder.newAddedProductAction(RemoteAddProductPayload(sSite, productModel)) + WCProductActionBuilder.newAddedProductAction( + RemoteAddProductPayload( + sSite, + productModel + ) + ) ) assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS)) } @@ -745,6 +812,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { assertEquals(TestEvent.FETCHED_PRODUCTS, nextEvent) mCountDownLatch.countDown() } + else -> throw AssertionError("Unexpected cause of change: " + event.causeOfChange) } } @@ -809,7 +877,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { fun onProductShippingClassesChanged(event: OnProductShippingClassesChanged) { event.error?.let { throw AssertionError( - "OnProductShippingClassesChanged has unexpected error: ${it.type}, ${it.message}" + "OnProductShippingClassesChanged has unexpected error: ${it.type}, ${it.message}" ) } @@ -820,10 +888,12 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { assertEquals(TestEvent.FETCHED_SINGLE_PRODUCT_SHIPPING_CLASS, nextEvent) mCountDownLatch.countDown() } + WCProductAction.FETCH_PRODUCT_SHIPPING_CLASS_LIST -> { assertEquals(TestEvent.FETCHED_PRODUCT_SHIPPING_CLASS_LIST, nextEvent) mCountDownLatch.countDown() } + else -> throw AssertionError("Unexpected cause of change: " + event.causeOfChange) } } @@ -842,10 +912,12 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { assertEquals(TestEvent.FETCH_PRODUCT_CATEGORIES, nextEvent) mCountDownLatch.countDown() } + WCProductAction.ADDED_PRODUCT_CATEGORY -> { assertEquals(TestEvent.ADDED_PRODUCT_CATEGORY, nextEvent) mCountDownLatch.countDown() } + else -> throw AssertionError("Unexpected cause of change: " + event.causeOfChange) } } @@ -855,7 +927,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { fun onProductTagChanged(event: OnProductTagChanged) { event.error?.let { throw AssertionError( - "OnProductTagChanged has unexpected error: ${it.type}, ${it.message}" + "OnProductTagChanged has unexpected error: ${it.type}, ${it.message}" ) } @@ -866,10 +938,12 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { assertEquals(TestEvent.FETCHED_PRODUCT_TAGS, nextEvent) mCountDownLatch.countDown() } + WCProductAction.ADDED_PRODUCT_TAGS -> { assertEquals(TestEvent.ADDED_PRODUCT_TAGS, nextEvent) mCountDownLatch.countDown() } + else -> throw AssertionError("Unexpected cause of change: " + event.causeOfChange) } } @@ -889,6 +963,7 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() { assertEquals(event.remoteProductId, productModel.remoteProductId) mCountDownLatch.countDown() } + else -> throw AssertionError("Unexpected cause of change: " + event.causeOfChange) } } diff --git a/example/src/main/java/org/wordpress/android/fluxc/example/ui/products/WooProductsFragment.kt b/example/src/main/java/org/wordpress/android/fluxc/example/ui/products/WooProductsFragment.kt index b35341618d..b9a0660489 100644 --- a/example/src/main/java/org/wordpress/android/fluxc/example/ui/products/WooProductsFragment.kt +++ b/example/src/main/java/org/wordpress/android/fluxc/example/ui/products/WooProductsFragment.kt @@ -307,7 +307,8 @@ class WooProductsFragment : StoreSelectingFragment() { FetchProductReviewsPayload( site, productIds = listOf(remoteProductId) - ) + ), + deletePreviouslyCachedReviews = false ) prependToLog("Fetched ${result.rowsAffected} product reviews") } @@ -321,7 +322,10 @@ class WooProductsFragment : StoreSelectingFragment() { coroutineScope.launch { prependToLog("Submitting request to fetch product reviews for site ${site.id}") val payload = FetchProductReviewsPayload(site) - val result = wcProductStore.fetchProductReviews(payload) + val result = wcProductStore.fetchProductReviews( + payload, + deletePreviouslyCachedReviews = false + ) prependToLog("Fetched ${result.rowsAffected} product reviews") } } diff --git a/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClientTest.kt b/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClientTest.kt index 9cfe7f6278..80822f5eb0 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClientTest.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClientTest.kt @@ -112,7 +112,7 @@ class SiteRestClientTest { } @Test - fun `returns fetched sites`() = test { + fun `returns fetched sites using filter`() = test { val response = SiteWPComRestResponse() response.ID = siteId val name = "Updated name" @@ -123,15 +123,18 @@ class SiteRestClientTest { sitesResponse.sites = listOf(response) initSitesResponse(data = sitesResponse) + initSitesFeaturesResponse(data = SitesFeaturesRestResponse(emptyMap())) val responseModel = restClient.fetchSites(listOf(WPCOM), false) assertThat(responseModel.sites).hasSize(1) assertThat(responseModel.sites[0].name).isEqualTo(name) assertThat(responseModel.sites[0].siteId).isEqualTo(siteId) - assertThat(urlCaptor.lastValue) + assertThat(urlCaptor.firstValue) .isEqualTo("https://public-api.wordpress.com/rest/v1.2/me/sites/") - assertThat(paramsCaptor.lastValue).isEqualTo( + assertThat(urlCaptor.lastValue) + .isEqualTo("https://public-api.wordpress.com/rest/v1.1/me/sites/features/") + assertThat(paramsCaptor.firstValue).isEqualTo( mapOf( "filters" to "wpcom", "fields" to "ID,URL,name,description,jetpack,jetpack_connection," + @@ -141,6 +144,35 @@ class SiteRestClientTest { ) } + @Test + fun `returns fetched sites when not filtering`() = test { + val response = SiteWPComRestResponse() + response.ID = siteId + val name = "Updated name" + response.name = name + response.URL = "site.com" + + val sitesResponse = SitesResponse() + sitesResponse.sites = listOf(response) + + initSitesResponse(data = sitesResponse) + + val responseModel = restClient.fetchSites(emptyList(), false) + assertThat(responseModel.sites).hasSize(1) + assertThat(responseModel.sites[0].name).isEqualTo(name) + assertThat(responseModel.sites[0].siteId).isEqualTo(siteId) + + assertThat(urlCaptor.firstValue) + .isEqualTo("https://public-api.wordpress.com/rest/v1.1/me/sites/") + assertThat(paramsCaptor.firstValue).isEqualTo( + mapOf( + "fields" to "ID,URL,name,description,jetpack,jetpack_connection," + + "visible,is_private,options,plan,capabilities,quota,icon,meta,zendesk_site_meta," + + "organization_id,was_ecommerce_trial" + ) + ) + } + @Test fun `fetched sites can filter JP connected package sites`() = test { val response = SiteWPComRestResponse() @@ -155,6 +187,7 @@ class SiteRestClientTest { sitesResponse.sites = listOf(response) initSitesResponse(data = sitesResponse) + initSitesFeaturesResponse(data = SitesFeaturesRestResponse(features = emptyMap())) val responseModel = restClient.fetchSites(listOf(WPCOM), true) @@ -521,6 +554,13 @@ class SiteRestClientTest { return initGetResponse(PostFormatsResponse::class.java, data ?: mock(), error) } + private suspend fun initSitesFeaturesResponse( + data: SitesFeaturesRestResponse? = null, + error: WPComGsonNetworkError? = null + ): Response { + return initGetResponse(SitesFeaturesRestResponse::class.java, data ?: mock(), error) + } + private suspend fun initGetResponse( clazz: Class, data: T, diff --git a/example/src/test/java/org/wordpress/android/fluxc/store/WCCustomerStoreTest.kt b/example/src/test/java/org/wordpress/android/fluxc/store/WCCustomerStoreTest.kt index 70248c998b..eeb87f77b0 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/store/WCCustomerStoreTest.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/store/WCCustomerStoreTest.kt @@ -21,7 +21,6 @@ import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType.INVALID_RE import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooPayload import org.wordpress.android.fluxc.network.rest.wpcom.wc.customer.CustomerRestClient import org.wordpress.android.fluxc.network.rest.wpcom.wc.customer.dto.CustomerDTO -import org.wordpress.android.fluxc.network.rest.wpcom.wc.customer.dto.CustomerFromAnalyticsDTO import org.wordpress.android.fluxc.persistence.CustomerSqlUtils import org.wordpress.android.fluxc.persistence.WellSqlConfig import org.wordpress.android.fluxc.test @@ -395,95 +394,6 @@ class WCCustomerStoreTest { assertEquals(customerModel, result.model) } - @Test - fun `given page 1, when fetchCustomersFromAnalytics, then result deleted and stored`() = - test { - // given - val siteModelId = 1 - val siteModel = SiteModel().apply { id = siteModelId } - val customerOne: CustomerFromAnalyticsDTO = mock() - val customerTwo: CustomerFromAnalyticsDTO = mock() - val response = arrayOf(customerOne, customerTwo) - whenever( - restClient.fetchCustomersFromAnalytics( - siteModel, - page = 1, - pageSize = 25 - ) - ).thenReturn(WooPayload(response)) - val modelOne = WCCustomerModel().apply { - remoteCustomerId = 1L - localSiteId = siteModelId - } - val modelTwo = WCCustomerModel().apply { - remoteCustomerId = 2L - localSiteId = siteModelId - } - whenever(mapper.mapToModel(siteModel, customerOne)).thenReturn(modelOne) - whenever(mapper.mapToModel(siteModel, customerTwo)).thenReturn(modelTwo) - - // when - val result = store.fetchCustomersFromAnalytics(siteModel, 1) - - // then - assertThat(result.isError).isFalse - assertThat(result.model).isEqualTo(listOf(modelOne, modelTwo)) - assertThat(CustomerSqlUtils.getCustomersForSite(siteModel)).isEqualTo( - listOf(modelOne, modelTwo) - ) - } - - @Test - fun `given page 1 then page 2, when fetchCustomersFromAnalytics, then both result stored`() = - test { - // given - val siteModelId = 1 - val siteModel = SiteModel().apply { id = siteModelId } - val customerOne: CustomerFromAnalyticsDTO = mock() - val customerTwo: CustomerFromAnalyticsDTO = mock() - val response = arrayOf(customerOne, customerTwo) - whenever( - restClient.fetchCustomersFromAnalytics( - siteModel, - page = 1, - pageSize = 25 - ) - ).thenReturn(WooPayload(response)) - whenever( - restClient.fetchCustomersFromAnalytics( - siteModel, - page = 2, - pageSize = 25 - ) - ).thenReturn(WooPayload(response)) - val modelOne = WCCustomerModel().apply { - remoteCustomerId = 1L - localSiteId = siteModelId - } - val modelTwo = WCCustomerModel().apply { - remoteCustomerId = 2L - localSiteId = siteModelId - } - whenever(mapper.mapToModel(siteModel, customerOne)).thenReturn(modelOne) - whenever(mapper.mapToModel(siteModel, customerTwo)).thenReturn(modelTwo) - - // when - val result = store.fetchCustomersFromAnalytics(siteModel, 1) - val result2 = store.fetchCustomersFromAnalytics(siteModel, 2) - - // then - assertThat(result.isError).isFalse - assertThat(result.model).isEqualTo(listOf(modelOne, modelTwo)) - assertThat(CustomerSqlUtils.getCustomersForSite(siteModel)).isEqualTo( - listOf(modelOne, modelTwo) - ) - assertThat(result2.isError).isFalse - assertThat(result2.model).isEqualTo(listOf(modelOne, modelTwo)) - assertThat(CustomerSqlUtils.getCustomersForSite(siteModel)).isEqualTo( - listOf(modelOne, modelTwo) - ) - } - @Test fun `given error, when fetchCustomersFromAnalytics, then nothing is stored and error`() = test { diff --git a/example/src/test/java/org/wordpress/android/fluxc/wc/taxes/TaxTestUtils.kt b/example/src/test/java/org/wordpress/android/fluxc/wc/taxes/TaxTestUtils.kt index 1ec2985f46..ecd4c50c49 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/wc/taxes/TaxTestUtils.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/wc/taxes/TaxTestUtils.kt @@ -1,6 +1,8 @@ package org.wordpress.android.fluxc.wc.taxes +import org.wordpress.android.fluxc.JsonLoaderUtils.jsonFileAs import org.wordpress.android.fluxc.model.taxes.WCTaxClassModel +import org.wordpress.android.fluxc.network.rest.wpcom.wc.taxes.TaxRateDto import org.wordpress.android.fluxc.network.rest.wpcom.wc.taxes.WCTaxRestClient.TaxClassApiResponse object TaxTestUtils { @@ -31,4 +33,9 @@ object TaxTestUtils { return listOf(TaxClassApiResponse("example1", "example1"), TaxClassApiResponse("example2", "example2")) } + + fun generateSampleTaxRateApiResponse() = + "wc/tax-rate-response.json" + .jsonFileAs(Array::class.java) } + diff --git a/example/src/test/java/org/wordpress/android/fluxc/wc/taxes/WCTaxStoreTest.kt b/example/src/test/java/org/wordpress/android/fluxc/wc/taxes/WCTaxStoreTest.kt index d85483cce8..8883cf8055 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/wc/taxes/WCTaxStoreTest.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/wc/taxes/WCTaxStoreTest.kt @@ -1,6 +1,7 @@ package org.wordpress.android.fluxc.wc.taxes import com.yarolegovich.wellsql.WellSql +import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -22,6 +23,7 @@ import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult import org.wordpress.android.fluxc.network.rest.wpcom.wc.taxes.WCTaxRestClient import org.wordpress.android.fluxc.persistence.SiteSqlUtils import org.wordpress.android.fluxc.persistence.WellSqlConfig +import org.wordpress.android.fluxc.persistence.dao.TaxRateDao import org.wordpress.android.fluxc.store.WCTaxStore import org.wordpress.android.fluxc.test import org.wordpress.android.fluxc.tools.initCoroutineEngine @@ -34,6 +36,7 @@ class WCTaxStoreTest { private val errorSite = SiteModel().apply { id = 123 } private val mapper = WCTaxClassMapper() private lateinit var store: WCTaxStore + private val taxRateDao = mock() private val sampleTaxClassList = TaxTestUtils.generateSampleTaxClassApiResponse() private val error = WooError(INVALID_RESPONSE, NETWORK_ERROR, "Invalid site ID") @@ -52,7 +55,8 @@ class WCTaxStoreTest { store = WCTaxStore( restClient, initCoroutineEngine(), - mapper + mapper, + taxRateDao ) // Insert the site into the db so it's available later when fetching tax classes @@ -86,6 +90,28 @@ class WCTaxStoreTest { assertThat(invalidRequestResult.size).isEqualTo(0) } + @Test + fun`when fetch tax rate fails, then error is returned` () { + runBlocking { + val error = WooError(INVALID_RESPONSE, NETWORK_ERROR, "Invalid site ID") + whenever(restClient.fetchTaxRateList(site, 1, 100)).thenReturn(WooPayload(error)) + val result = store.fetchTaxRateList(site, 1, 100) + assertThat(result.error).isEqualTo(error) + } + } + + @Test + fun `when fetch tax rate succeeds, then success returns` () { + runBlocking { + val taxRateApiResponse = TaxTestUtils.generateSampleTaxRateApiResponse() + whenever(restClient.fetchTaxRateList(site, 1, 100)).thenReturn(WooPayload(taxRateApiResponse)) + val result = store.fetchTaxRateList(site, 1, 100) + assertThat(this).isNotNull + assertThat(result.isError).isFalse + assertThat(result).isEqualTo(WooResult(false)) + } + } + private suspend fun fetchTaxClassListForSite(): WooResult> { val fetchTaxClassListPayload = WooPayload(sampleTaxClassList.toTypedArray()) whenever(restClient.fetchTaxClassList(site)).thenReturn(fetchTaxClassListPayload) diff --git a/example/src/test/resources/wc/tax-rate-response.json b/example/src/test/resources/wc/tax-rate-response.json new file mode 100644 index 0000000000..f83193043c --- /dev/null +++ b/example/src/test/resources/wc/tax-rate-response.json @@ -0,0 +1,33 @@ +[{ + "id":1, + "country":"US", + "state":"GA", + "postcode":"31707", + "city":"ALBANY", + "rate":"10.0000", + "name":"Superlong taaaaaaaax nameeeeee", + "priority":1, + "compound":false, + "shipping":true, + "order":0, + "class":"standard", + "postcodes":[ + "31707" + ], + "cities":[ + "ALBANY" + ], + "_links":{ + "self":[ + { + "href":"https://testwoomobile.wpcomstaging.com/wp-json/wc/v3/taxes/1" + } + ], + "collection":[ + { + "href":"https://testwoomobile.wpcomstaging.com/wp-json/wc/v3/taxes" + } + ] + } + } +] \ No newline at end of file diff --git a/fluxc-processor/src/main/resources/wp-com-endpoints.txt b/fluxc-processor/src/main/resources/wp-com-endpoints.txt index 1164690309..408903117f 100644 --- a/fluxc-processor/src/main/resources/wp-com-endpoints.txt +++ b/fluxc-processor/src/main/resources/wp-com-endpoints.txt @@ -19,6 +19,7 @@ /me/domain-contact-information/ /me/settings/ /me/sites/ +/me/sites/features /me/send-verification-email/ /me/social-login/connect/ /me/username/ diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClient.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClient.kt index 39c224ad27..2443f527f5 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClient.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/site/SiteWPAPIRestClient.kt @@ -72,7 +72,7 @@ class SiteWPAPIRestClient @Inject constructor( } wpApiRestUrl = discoveredWpApiUrl - this.url = response?.url ?: cleanedUrl.replaceBefore("://", urlScheme) + this.url = cleanedUrl.replaceBefore("://", urlScheme) this.username = payload.username this.password = payload.password } diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClient.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClient.kt index eb0470c063..8d440f4208 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClient.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClient.kt @@ -132,16 +132,43 @@ class SiteRestClient @Inject constructor( val site: SiteModel? = null ) : Payload() + /** + * Fetches the user's sites from WPCom. + * Since the V1.2 endpoint doesn't return the plan features, we will handle the fetch by following two + * different approaches: + * 1. If we don't need any filtering, then we'll simply use the v1.1 endpoint which includes the features. + * 2. If we have some filters, then we'll send two requests: the first one to the v1.2 endpoint to fetch sites + * And the second one to the /me/sites/features to fetch the features separately, the combine the results. + */ + @Suppress("ComplexMethod") suspend fun fetchSites(filters: List, filterJetpackConnectedPackageSite: Boolean): SitesModel { + val useV2Endpoint = filters.isNotEmpty() val params = getFetchSitesParams(filters) - val url = WPCOMREST.me.sites.urlV1_2 + val url = WPCOMREST.me.sites.let { if (useV2Endpoint) it.urlV1_2 else it.urlV1_1 } val response = wpComGsonRequestBuilder.syncGetRequest(this, url, params, SitesResponse::class.java) + + val siteFeatures = if (useV2Endpoint) { + fetchSitesFeatures().let { + if (it is Error) { + val result = SitesModel() + result.error = it.error + return result + } + (it as Success).data + } + } else null + return when (response) { is Success -> { val siteArray = mutableListOf() val jetpackCPSiteArray = mutableListOf() for (siteResponse in response.data.sites) { val siteModel = siteResponseToSiteModel(siteResponse) + + siteFeatures?.get(siteModel.siteId)?.let { + siteModel.planActiveFeatures = it.joinToString(",") + } + if (siteModel.isJetpackCPConnected) jetpackCPSiteArray.add(siteModel) // see https://github.com/wordpress-mobile/WordPress-Android/issues/15540#issuecomment-993752880 if (filterJetpackConnectedPackageSite && siteModel.isJetpackCPConnected) continue @@ -149,6 +176,7 @@ class SiteRestClient @Inject constructor( } SitesModel(siteArray, jetpackCPSiteArray) } + is Error -> { val payload = SitesModel(emptyList()) payload.error = response.error @@ -157,6 +185,21 @@ class SiteRestClient @Inject constructor( } } + private suspend fun fetchSitesFeatures(): Response>> { + val url = WPCOMREST.me.sites.features.urlV1_1 + return wpComGsonRequestBuilder.syncGetRequest( + restClient = this, + url = url, + params = emptyMap(), + clazz = SitesFeaturesRestResponse::class.java + ).let { + when (it) { + is Success -> Success(it.data.features.mapValues { it.value.active }) + is Error -> Error(it.error) + } + } + } + private fun getFetchSitesParams(filters: List): Map { val params = mutableMapOf() if (filters.isNotEmpty()) params[FILTERS] = TextUtils.join(",", filters) diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SitesFeaturesRestResponse.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SitesFeaturesRestResponse.kt new file mode 100644 index 0000000000..08141ef536 --- /dev/null +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SitesFeaturesRestResponse.kt @@ -0,0 +1,9 @@ +package org.wordpress.android.fluxc.network.rest.wpcom.site + +data class SitesFeaturesRestResponse( + val features: Map +) + +data class SiteFeatures( + val active: List +) diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/utils/MimeTypes.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/utils/MimeTypes.kt index a64215a0e9..7f70219eed 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/utils/MimeTypes.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/utils/MimeTypes.kt @@ -204,6 +204,9 @@ class MimeTypes { fun getAudioTypesOnly(plan: Plan = NO_PLAN_SPECIFIED) = (getAudioMimeTypesOnly(plan).toStrings()).toSet().toTypedArray() + fun getDocumentTypesOnly(plan: Plan = NO_PLAN_SPECIFIED) = + (getDocumentMimeTypesOnly(plan).toStrings()).toSet().toTypedArray() + private fun getAudioMimeTypesOnly(plan: Plan = NO_PLAN_SPECIFIED): List { return when (plan) { WP_COM_PAID, SELF_HOSTED, NO_PLAN_SPECIFIED -> audioTypes diff --git a/plugins/woocommerce/schemas/org.wordpress.android.fluxc.persistence.WCAndroidDatabase/27.json b/plugins/woocommerce/schemas/org.wordpress.android.fluxc.persistence.WCAndroidDatabase/27.json new file mode 100644 index 0000000000..399fd90ccf --- /dev/null +++ b/plugins/woocommerce/schemas/org.wordpress.android.fluxc.persistence.WCAndroidDatabase/27.json @@ -0,0 +1,1280 @@ +{ + "formatVersion": 1, + "database": { + "version": 27, + "identityHash": "f2c454ea3a1d74cf4b7ec999502708bc", + "entities": [ + { + "tableName": "AddonEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addonLocalId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `globalGroupLocalId` INTEGER, `productRemoteId` INTEGER, `localSiteId` INTEGER, `type` TEXT NOT NULL, `display` TEXT, `name` TEXT NOT NULL, `titleFormat` TEXT NOT NULL, `description` TEXT, `required` INTEGER NOT NULL, `position` INTEGER NOT NULL, `restrictions` TEXT, `priceType` TEXT, `price` TEXT, `min` INTEGER, `max` INTEGER, FOREIGN KEY(`globalGroupLocalId`) REFERENCES `GlobalAddonGroupEntity`(`globalGroupLocalId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "addonLocalId", + "columnName": "addonLocalId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "globalGroupLocalId", + "columnName": "globalGroupLocalId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "productRemoteId", + "columnName": "productRemoteId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "titleFormat", + "columnName": "titleFormat", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "required", + "columnName": "required", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictions", + "columnName": "restrictions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priceType", + "columnName": "priceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "addonLocalId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AddonEntity_globalGroupLocalId", + "unique": false, + "columnNames": [ + "globalGroupLocalId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddonEntity_globalGroupLocalId` ON `${TABLE_NAME}` (`globalGroupLocalId`)" + } + ], + "foreignKeys": [ + { + "table": "GlobalAddonGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "globalGroupLocalId" + ], + "referencedColumns": [ + "globalGroupLocalId" + ] + } + ] + }, + { + "tableName": "AddonOptionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addonOptionLocalId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `addonLocalId` INTEGER NOT NULL, `priceType` TEXT NOT NULL, `label` TEXT, `price` TEXT, `image` TEXT, FOREIGN KEY(`addonLocalId`) REFERENCES `AddonEntity`(`addonLocalId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "addonOptionLocalId", + "columnName": "addonOptionLocalId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addonLocalId", + "columnName": "addonLocalId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "priceType", + "columnName": "priceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "addonOptionLocalId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AddonOptionEntity_addonLocalId", + "unique": false, + "columnNames": [ + "addonLocalId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddonOptionEntity_addonLocalId` ON `${TABLE_NAME}` (`addonLocalId`)" + } + ], + "foreignKeys": [ + { + "table": "AddonEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "addonLocalId" + ], + "referencedColumns": [ + "addonLocalId" + ] + } + ] + }, + { + "tableName": "Coupons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localSiteId` INTEGER NOT NULL, `code` TEXT, `amount` TEXT, `dateCreated` TEXT, `dateCreatedGmt` TEXT, `dateModified` TEXT, `dateModifiedGmt` TEXT, `discountType` TEXT, `description` TEXT, `dateExpires` TEXT, `dateExpiresGmt` TEXT, `usageCount` INTEGER, `isForIndividualUse` INTEGER, `usageLimit` INTEGER, `usageLimitPerUser` INTEGER, `limitUsageToXItems` INTEGER, `isShippingFree` INTEGER, `areSaleItemsExcluded` INTEGER, `minimumAmount` TEXT, `maximumAmount` TEXT, `includedProductIds` TEXT, `excludedProductIds` TEXT, `includedCategoryIds` TEXT, `excludedCategoryIds` TEXT, PRIMARY KEY(`id`, `localSiteId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateCreatedGmt", + "columnName": "dateCreatedGmt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateModified", + "columnName": "dateModified", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateModifiedGmt", + "columnName": "dateModifiedGmt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "discountType", + "columnName": "discountType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateExpires", + "columnName": "dateExpires", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateExpiresGmt", + "columnName": "dateExpiresGmt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "usageCount", + "columnName": "usageCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isForIndividualUse", + "columnName": "isForIndividualUse", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usageLimit", + "columnName": "usageLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usageLimitPerUser", + "columnName": "usageLimitPerUser", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "limitUsageToXItems", + "columnName": "limitUsageToXItems", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isShippingFree", + "columnName": "isShippingFree", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "areSaleItemsExcluded", + "columnName": "areSaleItemsExcluded", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minimumAmount", + "columnName": "minimumAmount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumAmount", + "columnName": "maximumAmount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "includedProductIds", + "columnName": "includedProductIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "excludedProductIds", + "columnName": "excludedProductIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "includedCategoryIds", + "columnName": "includedCategoryIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "excludedCategoryIds", + "columnName": "excludedCategoryIds", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "localSiteId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CouponEmails", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`couponId` INTEGER NOT NULL, `localSiteId` INTEGER NOT NULL, `email` TEXT NOT NULL, PRIMARY KEY(`couponId`, `localSiteId`, `email`), FOREIGN KEY(`couponId`, `localSiteId`) REFERENCES `Coupons`(`id`, `localSiteId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "couponId", + "columnName": "couponId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "couponId", + "localSiteId", + "email" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "Coupons", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "couponId", + "localSiteId" + ], + "referencedColumns": [ + "id", + "localSiteId" + ] + } + ] + }, + { + "tableName": "GlobalAddonGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalGroupLocalId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `restrictedCategoriesIds` TEXT NOT NULL, `localSiteId` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "globalGroupLocalId", + "columnName": "globalGroupLocalId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "restrictedCategoriesIds", + "columnName": "restrictedCategoriesIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "globalGroupLocalId" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "OrderNotes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localSiteId` INTEGER NOT NULL, `noteId` INTEGER NOT NULL, `orderId` INTEGER NOT NULL, `dateCreated` TEXT, `note` TEXT, `author` TEXT, `isSystemNote` INTEGER NOT NULL, `isCustomerNote` INTEGER NOT NULL, PRIMARY KEY(`localSiteId`, `noteId`))", + "fields": [ + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "noteId", + "columnName": "noteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderId", + "columnName": "orderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSystemNote", + "columnName": "isSystemNote", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCustomerNote", + "columnName": "isCustomerNote", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "localSiteId", + "noteId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "OrderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localSiteId` INTEGER NOT NULL, `orderId` INTEGER NOT NULL, `number` TEXT NOT NULL, `status` TEXT NOT NULL, `currency` TEXT NOT NULL, `orderKey` TEXT NOT NULL, `dateCreated` TEXT NOT NULL, `dateModified` TEXT NOT NULL, `total` TEXT NOT NULL, `totalTax` TEXT NOT NULL, `shippingTotal` TEXT NOT NULL, `paymentMethod` TEXT NOT NULL, `paymentMethodTitle` TEXT NOT NULL, `datePaid` TEXT NOT NULL, `pricesIncludeTax` INTEGER NOT NULL, `customerNote` TEXT NOT NULL, `discountTotal` TEXT NOT NULL, `discountCodes` TEXT NOT NULL, `refundTotal` TEXT NOT NULL, `billingFirstName` TEXT NOT NULL, `billingLastName` TEXT NOT NULL, `billingCompany` TEXT NOT NULL, `billingAddress1` TEXT NOT NULL, `billingAddress2` TEXT NOT NULL, `billingCity` TEXT NOT NULL, `billingState` TEXT NOT NULL, `billingPostcode` TEXT NOT NULL, `billingCountry` TEXT NOT NULL, `billingEmail` TEXT NOT NULL, `billingPhone` TEXT NOT NULL, `shippingFirstName` TEXT NOT NULL, `shippingLastName` TEXT NOT NULL, `shippingCompany` TEXT NOT NULL, `shippingAddress1` TEXT NOT NULL, `shippingAddress2` TEXT NOT NULL, `shippingCity` TEXT NOT NULL, `shippingState` TEXT NOT NULL, `shippingPostcode` TEXT NOT NULL, `shippingCountry` TEXT NOT NULL, `shippingPhone` TEXT NOT NULL, `lineItems` TEXT NOT NULL, `shippingLines` TEXT NOT NULL, `feeLines` TEXT NOT NULL, `taxLines` TEXT NOT NULL, `couponLines` TEXT NOT NULL DEFAULT '', `metaData` TEXT NOT NULL, `paymentUrl` TEXT NOT NULL DEFAULT '', `isEditable` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`localSiteId`, `orderId`))", + "fields": [ + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderId", + "columnName": "orderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "orderKey", + "columnName": "orderKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateModified", + "columnName": "dateModified", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "totalTax", + "columnName": "totalTax", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingTotal", + "columnName": "shippingTotal", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentMethod", + "columnName": "paymentMethod", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentMethodTitle", + "columnName": "paymentMethodTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "datePaid", + "columnName": "datePaid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pricesIncludeTax", + "columnName": "pricesIncludeTax", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customerNote", + "columnName": "customerNote", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "discountTotal", + "columnName": "discountTotal", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "discountCodes", + "columnName": "discountCodes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refundTotal", + "columnName": "refundTotal", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingFirstName", + "columnName": "billingFirstName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingLastName", + "columnName": "billingLastName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingCompany", + "columnName": "billingCompany", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingAddress1", + "columnName": "billingAddress1", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingAddress2", + "columnName": "billingAddress2", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingCity", + "columnName": "billingCity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingState", + "columnName": "billingState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingPostcode", + "columnName": "billingPostcode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingCountry", + "columnName": "billingCountry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingEmail", + "columnName": "billingEmail", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "billingPhone", + "columnName": "billingPhone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingFirstName", + "columnName": "shippingFirstName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingLastName", + "columnName": "shippingLastName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingCompany", + "columnName": "shippingCompany", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingAddress1", + "columnName": "shippingAddress1", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingAddress2", + "columnName": "shippingAddress2", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingCity", + "columnName": "shippingCity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingState", + "columnName": "shippingState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingPostcode", + "columnName": "shippingPostcode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingCountry", + "columnName": "shippingCountry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingPhone", + "columnName": "shippingPhone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lineItems", + "columnName": "lineItems", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shippingLines", + "columnName": "shippingLines", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "feeLines", + "columnName": "feeLines", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "taxLines", + "columnName": "taxLines", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "couponLines", + "columnName": "couponLines", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "metaData", + "columnName": "metaData", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentUrl", + "columnName": "paymentUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isEditable", + "columnName": "isEditable", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "columnNames": [ + "localSiteId", + "orderId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_OrderEntity_localSiteId_orderId", + "unique": false, + "columnNames": [ + "localSiteId", + "orderId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_OrderEntity_localSiteId_orderId` ON `${TABLE_NAME}` (`localSiteId`, `orderId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "OrderMetaData", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localSiteId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `orderId` INTEGER NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, `isDisplayable` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`localSiteId`, `orderId`, `id`), FOREIGN KEY(`localSiteId`, `orderId`) REFERENCES `OrderEntity`(`localSiteId`, `orderId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderId", + "columnName": "orderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDisplayable", + "columnName": "isDisplayable", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "columnNames": [ + "localSiteId", + "orderId", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_OrderMetaData_localSiteId_orderId", + "unique": false, + "columnNames": [ + "localSiteId", + "orderId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_OrderMetaData_localSiteId_orderId` ON `${TABLE_NAME}` (`localSiteId`, `orderId`)" + } + ], + "foreignKeys": [ + { + "table": "OrderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "localSiteId", + "orderId" + ], + "referencedColumns": [ + "localSiteId", + "orderId" + ] + } + ] + }, + { + "tableName": "InboxNotes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `remoteId` INTEGER NOT NULL, `localSiteId` INTEGER NOT NULL, `name` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `dateCreated` TEXT NOT NULL, `status` TEXT NOT NULL, `source` TEXT, `type` TEXT, `dateReminder` TEXT)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "localId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateReminder", + "columnName": "dateReminder", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "localId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_InboxNotes_remoteId_localSiteId", + "unique": true, + "columnNames": [ + "remoteId", + "localSiteId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_InboxNotes_remoteId_localSiteId` ON `${TABLE_NAME}` (`remoteId`, `localSiteId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InboxNoteActions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remoteId` INTEGER NOT NULL, `inboxNoteLocalId` INTEGER NOT NULL, `localSiteId` INTEGER NOT NULL, `name` TEXT NOT NULL, `label` TEXT NOT NULL, `url` TEXT NOT NULL, `query` TEXT, `status` TEXT, `primary` INTEGER NOT NULL, `actionedText` TEXT, PRIMARY KEY(`remoteId`, `inboxNoteLocalId`), FOREIGN KEY(`inboxNoteLocalId`) REFERENCES `InboxNotes`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inboxNoteLocalId", + "columnName": "inboxNoteLocalId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "actionedText", + "columnName": "actionedText", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "remoteId", + "inboxNoteLocalId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_InboxNoteActions_inboxNoteLocalId", + "unique": false, + "columnNames": [ + "inboxNoteLocalId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_InboxNoteActions_inboxNoteLocalId` ON `${TABLE_NAME}` (`inboxNoteLocalId`)" + } + ], + "foreignKeys": [ + { + "table": "InboxNotes", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "inboxNoteLocalId" + ], + "referencedColumns": [ + "localId" + ] + } + ] + }, + { + "tableName": "TopPerformerProducts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localSiteId` INTEGER NOT NULL, `datePeriod` TEXT NOT NULL, `productId` INTEGER NOT NULL, `name` TEXT NOT NULL, `imageUrl` TEXT, `quantity` INTEGER NOT NULL, `currency` TEXT NOT NULL, `total` REAL NOT NULL, `millisSinceLastUpdated` INTEGER NOT NULL, PRIMARY KEY(`datePeriod`, `productId`, `localSiteId`))", + "fields": [ + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "datePeriod", + "columnName": "datePeriod", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "productId", + "columnName": "productId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "millisSinceLastUpdated", + "columnName": "millisSinceLastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "datePeriod", + "productId", + "localSiteId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TaxBasedOnSetting", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localSiteId` INTEGER NOT NULL, `selectedOption` TEXT NOT NULL, PRIMARY KEY(`localSiteId`))", + "fields": [ + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedOption", + "columnName": "selectedOption", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "localSiteId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TaxRate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localSiteId` INTEGER NOT NULL, `country` TEXT, `state` TEXT, `postcode` TEXT, `city` TEXT, `rate` TEXT, `name` TEXT, `taxClass` TEXT, PRIMARY KEY(`id`, `localSiteId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localSiteId", + "columnName": "localSiteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "country", + "columnName": "country", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postcode", + "columnName": "postcode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "city", + "columnName": "city", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rate", + "columnName": "rate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "taxClass", + "columnName": "taxClass", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "localSiteId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f2c454ea3a1d74cf4b7ec999502708bc')" + ] + } +} \ No newline at end of file diff --git a/plugins/woocommerce/src/androidTest/java/org/wordpress/android/fluxc/persistence/MigrationTests.kt b/plugins/woocommerce/src/androidTest/java/org/wordpress/android/fluxc/persistence/MigrationTests.kt index c272e8abf3..c152af133d 100644 --- a/plugins/woocommerce/src/androidTest/java/org/wordpress/android/fluxc/persistence/MigrationTests.kt +++ b/plugins/woocommerce/src/androidTest/java/org/wordpress/android/fluxc/persistence/MigrationTests.kt @@ -227,6 +227,14 @@ class MigrationTests { } } + @Test + fun testMigrate26to27() { + helper.apply { + createDatabase(TEST_DB, 26).close() + runMigrationsAndValidate(TEST_DB, 27, false) + } + } + companion object { private const val TEST_DB = "migration-test" } diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/di/WCDatabaseModule.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/di/WCDatabaseModule.kt index f2f6b4d55a..84258603a9 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/di/WCDatabaseModule.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/di/WCDatabaseModule.kt @@ -44,6 +44,8 @@ interface WCDatabaseModule { @Provides fun provideTopPerformerProductsDao(database: WCAndroidDatabase) = database.topPerformerProductsDao @Provides fun provideTaxBasedOnDao(database: WCAndroidDatabase) = database.taxBasedOnSettingDao + + @Provides fun provideTaxRateDao(database: WCAndroidDatabase) = database.taxRateDao } @Binds fun bindTransactionExecutor(database: WCAndroidDatabase): TransactionExecutor } diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/StripProductMetaData.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/StripProductMetaData.kt index 64fd1c64f6..9c1371712b 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/StripProductMetaData.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/StripProductMetaData.kt @@ -3,9 +3,10 @@ package org.wordpress.android.fluxc.model import com.google.gson.Gson import com.google.gson.JsonArray import com.google.gson.JsonObject -import org.wordpress.android.fluxc.model.WCProductModel.SubscriptionMetadataKeys import org.wordpress.android.fluxc.model.WCProductModel.AddOnsMetadataKeys +import org.wordpress.android.fluxc.model.WCProductModel.OtherKeys import org.wordpress.android.fluxc.model.WCProductModel.QuantityRulesMetadataKeys +import org.wordpress.android.fluxc.model.WCProductModel.SubscriptionMetadataKeys import org.wordpress.android.fluxc.utils.EMPTY_JSON_ARRAY import org.wordpress.android.fluxc.utils.isElementNullOrEmpty import javax.inject.Inject @@ -30,6 +31,7 @@ class StripProductMetaData @Inject internal constructor(private val gson: Gson) add(AddOnsMetadataKeys.ADDONS_METADATA_KEY) addAll(QuantityRulesMetadataKeys.ALL_KEYS) addAll(SubscriptionMetadataKeys.ALL_KEYS) + add(OtherKeys.HEAD_START_POST) } } } diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/WCProductModel.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/WCProductModel.kt index 3f696fa1d5..9c84c4c628 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/WCProductModel.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/WCProductModel.kt @@ -119,6 +119,11 @@ data class WCProductModel(@PrimaryKey @Column private var id: Int = 0) : Identif ?.find { it.key == ADDONS_METADATA_KEY } ?.addons + val isSampleProduct: Boolean + get() = Gson().fromJson(metadata, Array::class.java) + ?.any { it.key == OtherKeys.HEAD_START_POST && it.value == "_hs_extra" } + ?: false + @Suppress("SwallowedException", "TooGenericExceptionCaught") private val WCMetaData.addons get() = try { @@ -596,4 +601,8 @@ data class WCProductModel(@PrimaryKey @Column private var id: Int = 0) : Identif ALLOW_COMBINATION ) } + + object OtherKeys { + const val HEAD_START_POST = "_headstart_post" + } } diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/taxes/TaxRateEntity.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/taxes/TaxRateEntity.kt new file mode 100644 index 0000000000..54dc8ae5ce --- /dev/null +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/taxes/TaxRateEntity.kt @@ -0,0 +1,21 @@ +package org.wordpress.android.fluxc.model.taxes + +import androidx.room.Entity +import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId +import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId + +@Entity( + tableName = "TaxRate", + primaryKeys = ["id", "localSiteId"], +) +data class TaxRateEntity ( + val id: RemoteId, + val localSiteId: LocalId, + val country: String? = null, + val state: String? = null, + val postcode: String? = null, + val city: String? = null, + val rate: String? = null, + val name: String? = null, + val taxClass: String? = null, +) diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductRestClient.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductRestClient.kt index fa4cce4d93..c6bafb484c 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductRestClient.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductRestClient.kt @@ -1488,8 +1488,7 @@ class ProductRestClient @Inject constructor( reviews, productIds, filterByStatus, - offset > 0, - reviews.size == WCProductStore.NUM_REVIEWS_PER_FETCH + canLoadMore = reviews.size == WCProductStore.NUM_REVIEWS_PER_FETCH ) } else { FetchProductReviewsResponsePayload( diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/attributes/ProductAttributeRestClient.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/attributes/ProductAttributeRestClient.kt index 3b3044b3f5..38bb3f3d48 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/attributes/ProductAttributeRestClient.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/attributes/ProductAttributeRestClient.kt @@ -42,9 +42,17 @@ class ProductAttributeRestClient @Inject constructor(private val wooNetwork: Woo suspend fun fetchAllAttributeTerms( site: SiteModel, - attributeID: Long + attributeID: Long, + page: Int, + pageSize: Int ) = WOOCOMMERCE.products.attributes.attribute(attributeID).terms.pathV3 - .request>(site) + .request>( + site = site, + params = mapOf( + "page" to page.toString(), + "per_page" to pageSize.toString() + ) + ) suspend fun postNewTerm( site: SiteModel, @@ -69,11 +77,13 @@ class ProductAttributeRestClient @Inject constructor(private val wooNetwork: Woo .delete(site) private suspend inline fun String.request( - site: SiteModel + site: SiteModel, + params: Map = emptyMap() ) = wooNetwork.executeGetGsonRequest( site = site, path = this, - clazz = T::class.java + clazz = T::class.java, + params = params ).toWooPayload() private suspend inline fun String.post( diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/taxes/TaxRateDto.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/taxes/TaxRateDto.kt new file mode 100644 index 0000000000..6b75b7fb94 --- /dev/null +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/taxes/TaxRateDto.kt @@ -0,0 +1,38 @@ +package org.wordpress.android.fluxc.network.rest.wpcom.wc.taxes + +import com.google.gson.annotations.SerializedName +import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId +import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId +import org.wordpress.android.fluxc.model.taxes.TaxRateEntity + +data class TaxRateDto ( + val id: Long, + val country: String?, + val state: String?, + @SerializedName("postcode") val postCode: String?, + val city: String?, + @SerializedName("postcodes") val postCodes: List?, + val cities: List?, + val rate: String?, + val name: String?, + val priority: Int?, + val compound: Boolean?, + val shipping: Boolean?, + val order: Int?, + @SerializedName("class") val taxClass: String?, +) { + fun toDataModel(localSiteId: LocalId): TaxRateEntity = + TaxRateEntity( + id = RemoteId(id), + localSiteId = localSiteId, + country = country, + state = state, + postcode = postCode, + city = city, + rate = rate, + name = name, + taxClass = taxClass, + ) +} + + diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/taxes/WCTaxRestClient.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/taxes/WCTaxRestClient.kt index 32a25ac72a..99994b6169 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/taxes/WCTaxRestClient.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/taxes/WCTaxRestClient.kt @@ -2,8 +2,10 @@ package org.wordpress.android.fluxc.network.rest.wpcom.wc.taxes import org.wordpress.android.fluxc.generated.endpoint.WOOCOMMERCE import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.rest.wpapi.WPAPIResponse 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.network.rest.wpcom.wc.toWooError import org.wordpress.android.fluxc.utils.toWooPayload import javax.inject.Inject import javax.inject.Singleton @@ -23,6 +25,32 @@ class WCTaxRestClient @Inject constructor(private val wooNetwork: WooNetwork) { return response.toWooPayload() } + suspend fun fetchTaxRateList( + site: SiteModel, + page: Int, + pageSize: Int, + ): WooPayload> { + val url = WOOCOMMERCE.taxes.pathV3 + + val response = wooNetwork.executeGetGsonRequest( + site, + url, + Array::class.java, + mutableMapOf().apply { + put("page", page.toString()) + put("per_page", pageSize.toString()) + } + ) + return when (response) { + is WPAPIResponse.Success -> { + WooPayload(response.data) + } + is WPAPIResponse.Error -> { + WooPayload(response.error.toWooError()) + } + } + } + data class TaxClassApiResponse( val name: String? = null, val slug: String? = null diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/CustomerSqlUtils.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/CustomerSqlUtils.kt index 26fb079c2e..bdb603e5ce 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/CustomerSqlUtils.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/CustomerSqlUtils.kt @@ -65,7 +65,13 @@ object CustomerSqlUtils { } fun insertOrUpdateCustomers(customers: List): Int { - return customers.sumBy { insertOrUpdateCustomer(it) } + return customers.sumOf { insertOrUpdateCustomer(it) } + } + + fun insertCustomers(customers: List) { + customers.forEach { + WellSql.insert(it).asSingleTransaction(true).execute() + } } // endregion diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/ProductSqlUtils.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/ProductSqlUtils.kt index 36adf414e4..9bd0990c0c 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/ProductSqlUtils.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/ProductSqlUtils.kt @@ -491,6 +491,15 @@ object ProductSqlUtils { .asModel } + fun getProductReviewsByReviewIds(reviewIds: List): List { + return WellSql.select(WCProductReviewModel::class.java) + .where() + .isIn(WCProductReviewModelTable.REMOTE_PRODUCT_REVIEW_ID, reviewIds) + .endWhere() + .orderBy(WCProductReviewModelTable.DATE_CREATED, SelectQuery.ORDER_DESCENDING) + .asModel + } + fun getProductReviewsForProductAndSiteId( localSiteId: Int, remoteProductId: Long diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/WCAndroidDatabase.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/WCAndroidDatabase.kt index bd9facc6ab..1e06c22d1d 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/WCAndroidDatabase.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/WCAndroidDatabase.kt @@ -9,6 +9,7 @@ import androidx.room.TypeConverters import androidx.room.withTransaction import org.wordpress.android.fluxc.model.OrderEntity import org.wordpress.android.fluxc.model.taxes.TaxBasedOnSettingEntity +import org.wordpress.android.fluxc.model.taxes.TaxRateEntity import org.wordpress.android.fluxc.persistence.converters.BigDecimalConverter import org.wordpress.android.fluxc.persistence.converters.LocalIdConverter import org.wordpress.android.fluxc.persistence.converters.LongListConverter @@ -20,6 +21,7 @@ import org.wordpress.android.fluxc.persistence.dao.OrderMetaDataDao import org.wordpress.android.fluxc.persistence.dao.OrderNotesDao import org.wordpress.android.fluxc.persistence.dao.OrdersDao import org.wordpress.android.fluxc.persistence.dao.TaxBasedOnDao +import org.wordpress.android.fluxc.persistence.dao.TaxRateDao import org.wordpress.android.fluxc.persistence.dao.TopPerformerProductsDao import org.wordpress.android.fluxc.persistence.entity.AddonEntity import org.wordpress.android.fluxc.persistence.entity.AddonOptionEntity @@ -55,7 +57,7 @@ import org.wordpress.android.fluxc.persistence.migrations.MIGRATION_8_9 import org.wordpress.android.fluxc.persistence.migrations.MIGRATION_9_10 @Database( - version = 26, + version = 27, entities = [ AddonEntity::class, AddonOptionEntity::class, @@ -69,6 +71,7 @@ import org.wordpress.android.fluxc.persistence.migrations.MIGRATION_9_10 InboxNoteActionEntity::class, TopPerformerProductEntity::class, TaxBasedOnSettingEntity::class, + TaxRateEntity::class ], autoMigrations = [ AutoMigration(from = 12, to = 13), @@ -80,6 +83,7 @@ import org.wordpress.android.fluxc.persistence.migrations.MIGRATION_9_10 AutoMigration(from = 19, to = 20, spec = AutoMigration19to20::class), AutoMigration(from = 23, to = 24, spec = AutoMigration23to24::class), AutoMigration(from = 25, to = 26), + AutoMigration(from = 26, to = 27), ] ) @TypeConverters( @@ -99,6 +103,7 @@ abstract class WCAndroidDatabase : RoomDatabase(), TransactionExecutor { abstract val inboxNotesDao: InboxNotesDao abstract val topPerformerProductsDao: TopPerformerProductsDao abstract val taxBasedOnSettingDao: TaxBasedOnDao + abstract val taxRateDao: TaxRateDao companion object { fun buildDb(applicationContext: Context) = Room.databaseBuilder( diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/dao/TaxRateDao.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/dao/TaxRateDao.kt new file mode 100644 index 0000000000..45aa43dbd3 --- /dev/null +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/persistence/dao/TaxRateDao.kt @@ -0,0 +1,31 @@ +package org.wordpress.android.fluxc.persistence.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow +import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId +import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId +import org.wordpress.android.fluxc.model.taxes.TaxRateEntity + +@Dao +interface TaxRateDao { + @Transaction + @Query("SELECT * FROM TaxRate WHERE localSiteId = :localSiteId") + fun observeTaxRates(localSiteId: LocalId): Flow> + + @Query("SELECT * FROM TaxRate WHERE localSiteId = :localSiteId") + suspend fun getTaxRates(localSiteId: LocalId): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertOrUpdate(taxRate: TaxRateEntity): Long + + @Query("DELETE FROM TaxRate WHERE localSiteId = :localSiteId") + suspend fun deleteAll(localSiteId: LocalId) + + @Transaction + @Query("SELECT * FROM TaxRate WHERE localSiteId = :localSiteId AND id = :taxRateId") + suspend fun getTaxRate(localSiteId: LocalId, taxRateId: RemoteId): TaxRateEntity? +} diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCCustomerStore.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCCustomerStore.kt index d46949e04a..63693e8b82 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCCustomerStore.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCCustomerStore.kt @@ -42,6 +42,14 @@ class WCCustomerStore @Inject constructor( fun getCustomerByRemoteIds(site: SiteModel, remoteCustomerId: List) = CustomerSqlUtils.getCustomerByRemoteIds(site, remoteCustomerId) + fun saveCustomers(customers: List) { + CustomerSqlUtils.insertCustomers(customers) + } + + fun deleteCustomersForSite(site: SiteModel) { + CustomerSqlUtils.deleteCustomersForSite(site) + } + /** * returns a customer with provided remote id */ @@ -201,11 +209,7 @@ class WCCustomerStore @Inject constructor( WooResult(response.error) } response.result != null -> { - val customers = response.result.map { mapper.mapToModel(site, it) } - if (page == 1) CustomerSqlUtils.deleteCustomersForSite(site) - CustomerSqlUtils.insertOrUpdateCustomers(customers) - - WooResult(customers) + WooResult(response.result.map { mapper.mapToModel(site, it) }) } else -> WooResult(WooError(GENERIC_ERROR, UNKNOWN)) } diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCGlobalAttributeStore.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCGlobalAttributeStore.kt index 3c2f0dcf07..d6390b6526 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCGlobalAttributeStore.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCGlobalAttributeStore.kt @@ -54,8 +54,10 @@ class WCGlobalAttributeStore @Inject constructor( suspend fun fetchAttributeTerms( site: SiteModel, - attributeID: Long - ) = restClient.fetchAllAttributeTerms(site, attributeID) + attributeID: Long, + page: Int = DEFAULT_PAGE_INDEX, + pageSize: Int = DEFAULT_PAGE_SIZE + ) = restClient.fetchAllAttributeTerms(site, attributeID, page, pageSize) .result?.map { mapper.responseToAttributeTermModel(it, attributeID.toInt(), site) } ?.apply { insertAttributeTermsFromScratch(attributeID.toInt(), site.id, this) @@ -192,4 +194,9 @@ class WCGlobalAttributeStore @Inject constructor( .model ?.let { updateSingleAttributeTermsMapping(attributeID.toInt(), termsId, site.id) } } + + companion object { + const val DEFAULT_PAGE_SIZE = 100 + const val DEFAULT_PAGE_INDEX = 1 + } } diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCProductStore.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCProductStore.kt index cde9d6d395..265aa8be93 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCProductStore.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCProductStore.kt @@ -84,6 +84,7 @@ class WCProductStore @Inject constructor( override fun toString() = name.toLowerCase(Locale.US) } + enum class SkuSearchOptions { Disabled, ExactSearch, PartialMatch } @@ -567,7 +568,6 @@ class WCProductStore @Inject constructor( val reviews: List = emptyList(), val filterProductIds: List? = null, val filterByStatus: List? = null, - val loadedMore: Boolean = false, val canLoadMore: Boolean = false ) : Payload() { constructor(error: ProductError, site: SiteModel) : this(site) { @@ -825,6 +825,9 @@ class WCProductStore @Inject constructor( fun getProductReviewsForSite(site: SiteModel): List = ProductSqlUtils.getProductReviewsForSite(site) + fun getProductReviewsByReviewId(reviewIds: List): List = + ProductSqlUtils.getProductReviewsByReviewIds(reviewIds) + fun getProductReviewsForProductAndSiteId(localSiteId: Int, remoteProductId: Long): List = ProductSqlUtils.getProductReviewsForProductAndSiteId(localSiteId, remoteProductId) @@ -887,64 +890,92 @@ class WCProductStore @Inject constructor( // remote actions WCProductAction.FETCH_PRODUCT_SKU_AVAILABILITY -> fetchProductSkuAvailability(action.payload as FetchProductSkuAvailabilityPayload) + WCProductAction.FETCH_PRODUCTS -> fetchProducts(action.payload as FetchProductsPayload) + WCProductAction.SEARCH_PRODUCTS -> searchProducts(action.payload as SearchProductsPayload) + WCProductAction.UPDATE_PRODUCT_IMAGES -> updateProductImages(action.payload as UpdateProductImagesPayload) + WCProductAction.UPDATE_PRODUCT -> updateProduct(action.payload as UpdateProductPayload) + WCProductAction.FETCH_SINGLE_PRODUCT_SHIPPING_CLASS -> fetchProductShippingClass(action.payload as FetchSingleProductShippingClassPayload) + WCProductAction.FETCH_PRODUCT_SHIPPING_CLASS_LIST -> fetchProductShippingClasses(action.payload as FetchProductShippingClassListPayload) + WCProductAction.FETCH_PRODUCT_PASSWORD -> fetchProductPassword(action.payload as FetchProductPasswordPayload) + WCProductAction.UPDATE_PRODUCT_PASSWORD -> updateProductPassword(action.payload as UpdateProductPasswordPayload) + WCProductAction.FETCH_PRODUCT_CATEGORIES -> fetchProductCategories(action.payload as FetchProductCategoriesPayload) + WCProductAction.ADD_PRODUCT_CATEGORY -> addProductCategory(action.payload as AddProductCategoryPayload) + WCProductAction.FETCH_PRODUCT_TAGS -> fetchProductTags(action.payload as FetchProductTagsPayload) + WCProductAction.ADD_PRODUCT_TAGS -> addProductTags(action.payload as AddProductTagsPayload) + WCProductAction.ADD_PRODUCT -> addProduct(action.payload as AddProductPayload) + WCProductAction.DELETE_PRODUCT -> deleteProduct(action.payload as DeleteProductPayload) // remote responses WCProductAction.FETCHED_PRODUCT_SKU_AVAILABILITY -> handleFetchProductSkuAvailabilityCompleted(action.payload as RemoteProductSkuAvailabilityPayload) + WCProductAction.FETCHED_PRODUCTS -> handleFetchProductsCompleted(action.payload as RemoteProductListPayload) + WCProductAction.SEARCHED_PRODUCTS -> handleSearchProductsCompleted(action.payload as RemoteSearchProductsPayload) + WCProductAction.UPDATED_PRODUCT_IMAGES -> handleUpdateProductImages(action.payload as RemoteUpdateProductImagesPayload) + WCProductAction.UPDATED_PRODUCT -> handleUpdateProduct(action.payload as RemoteUpdateProductPayload) + WCProductAction.FETCHED_PRODUCT_SHIPPING_CLASS_LIST -> handleFetchProductShippingClassesCompleted(action.payload as RemoteProductShippingClassListPayload) + WCProductAction.FETCHED_SINGLE_PRODUCT_SHIPPING_CLASS -> handleFetchProductShippingClassCompleted(action.payload as RemoteProductShippingClassPayload) + WCProductAction.FETCHED_PRODUCT_PASSWORD -> handleFetchProductPasswordCompleted(action.payload as RemoteProductPasswordPayload) + WCProductAction.UPDATED_PRODUCT_PASSWORD -> handleUpdatedProductPasswordCompleted(action.payload as RemoteUpdatedProductPasswordPayload) + WCProductAction.FETCHED_PRODUCT_CATEGORIES -> handleFetchProductCategories(action.payload as RemoteProductCategoriesPayload) + WCProductAction.ADDED_PRODUCT_CATEGORY -> handleAddProductCategory(action.payload as RemoteAddProductCategoryResponsePayload) + WCProductAction.FETCHED_PRODUCT_TAGS -> handleFetchProductTagsCompleted(action.payload as RemoteProductTagsPayload) + WCProductAction.ADDED_PRODUCT_TAGS -> handleAddProductTags(action.payload as RemoteAddProductTagsResponsePayload) + WCProductAction.ADDED_PRODUCT -> handleAddNewProduct(action.payload as RemoteAddProductPayload) + WCProductAction.DELETED_PRODUCT -> handleDeleteProduct(action.payload as RemoteDeleteProductPayload) } @@ -1189,7 +1220,10 @@ class WCProductStore @Inject constructor( with(payload) { wcProductRestClient.fetchProductShippingClassList(site, pageSize, offset) } } - suspend fun fetchProductReviews(payload: FetchProductReviewsPayload): OnProductReviewChanged { + suspend fun fetchProductReviews( + payload: FetchProductReviewsPayload, + deletePreviouslyCachedReviews: Boolean + ): OnProductReviewChanged { return coroutineEngine.withDefaultContext(API, this, "fetchProductReviews") { val response = with(payload) { wcProductRestClient.fetchProductReviews(site, offset, reviewIds, productIds, filterByStatus) @@ -1200,8 +1234,8 @@ class WCProductStore @Inject constructor( } else { // Clear existing product reviews if this is a fresh fetch (loadMore = false). // This is the simplest way to keep our local reviews in sync with remote reviews - // in case of deletions. - if (!response.loadedMore) { + // in case of deletions or status updates. + if (deletePreviouslyCachedReviews) { ProductSqlUtils.deleteAllProductReviewsForSite(response.site) } val rowsAffected = ProductSqlUtils.insertOrUpdateProductReviews(response.reviews) @@ -1447,6 +1481,7 @@ class WCProductStore @Inject constructor( val canLoadMore = response.result.size == pageSize WooResult(canLoadMore) } + else -> WooResult(WooError(WooErrorType.GENERIC_ERROR, UNKNOWN)) } } @@ -1492,6 +1527,7 @@ class WCProductStore @Inject constructor( val canLoadMore = response.result.size == pageSize WooResult(canLoadMore) } + else -> WooResult(WooError(WooErrorType.GENERIC_ERROR, UNKNOWN)) } } @@ -1525,6 +1561,7 @@ class WCProductStore @Inject constructor( val canLoadMore = response.result.size == pageSize WooResult(ProductSearchResult(products, canLoadMore)) } + else -> WooResult(WooError(WooErrorType.GENERIC_ERROR, UNKNOWN)) } } @@ -1560,6 +1597,7 @@ class WCProductStore @Inject constructor( val canLoadMore = response.result.size == pageSize WooResult(ProductCategorySearchResult(categories, canLoadMore)) } + else -> WooResult(WooError(WooErrorType.GENERIC_ERROR, UNKNOWN)) } } @@ -1599,6 +1637,7 @@ class WCProductStore @Inject constructor( val canLoadMore = response.result.size == pageSize WooResult(canLoadMore) } + else -> WooResult(WooError(WooErrorType.GENERIC_ERROR, UNKNOWN)) } } @@ -1649,6 +1688,7 @@ class WCProductStore @Inject constructor( response.result != null -> { WooResult(response.result.sumOf { it.total }) } + else -> WooResult(WooError(WooErrorType.GENERIC_ERROR, UNKNOWN)) } } diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCTaxStore.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCTaxStore.kt index 4c4f9ecb8e..e83270c146 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCTaxStore.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCTaxStore.kt @@ -1,16 +1,23 @@ package org.wordpress.android.fluxc.store +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.taxes.TaxRateEntity import org.wordpress.android.fluxc.model.taxes.WCTaxClassMapper import org.wordpress.android.fluxc.model.taxes.WCTaxClassModel 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 +import org.wordpress.android.fluxc.network.rest.wpcom.wc.taxes.TaxRateDto import org.wordpress.android.fluxc.network.rest.wpcom.wc.taxes.WCTaxRestClient import org.wordpress.android.fluxc.persistence.WCTaxSqlUtils +import org.wordpress.android.fluxc.persistence.dao.TaxRateDao import org.wordpress.android.fluxc.tools.CoroutineEngine -import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T.API import javax.inject.Inject import javax.inject.Singleton @@ -18,21 +25,28 @@ import javax.inject.Singleton class WCTaxStore @Inject constructor( private val restClient: WCTaxRestClient, private val coroutineEngine: CoroutineEngine, - private val mapper: WCTaxClassMapper + private val mapper: WCTaxClassMapper, + private val taxRateDao: TaxRateDao, ) { + companion object { + const val DEFAULT_PAGE_SIZE = 100 + const val DEFAULT_PAGE = 1 + } + /** * returns a list of tax classes for a specific site in the database */ fun getTaxClassListForSite(site: SiteModel): List = - WCTaxSqlUtils.getTaxClassesForSite(site.id) + WCTaxSqlUtils.getTaxClassesForSite(site.id) suspend fun fetchTaxClassList(site: SiteModel): WooResult> { - return coroutineEngine.withDefaultContext(AppLog.T.API, this, "fetchTaxClassList") { + return coroutineEngine.withDefaultContext(API, this, "fetchTaxClassList") { val response = restClient.fetchTaxClassList(site) return@withDefaultContext when { response.isError -> { WooResult(response.error) } + response.result != null -> { val taxClassModels = response.result.map { mapper.map(it).apply { localSiteId = site.id } @@ -43,8 +57,47 @@ class WCTaxStore @Inject constructor( WCTaxSqlUtils.insertOrUpdateTaxClasses(taxClassModels) WooResult(taxClassModels) } + + else -> WooResult(WooError(GENERIC_ERROR, UNKNOWN)) + } + } + } + + /** + * returns a boolean indicating whether more Tax Rates can be fetched. + */ + suspend fun fetchTaxRateList( + site: SiteModel, + page: Int = DEFAULT_PAGE, + pageSize: Int = DEFAULT_PAGE_SIZE + ): WooResult { + return coroutineEngine.withDefaultContext(API, this, "fetchTaxRateList") { + val response = restClient.fetchTaxRateList(site, page, pageSize) + when { + response.isError -> WooResult(response.error) + response.result != null -> { + if (page == 1) { + taxRateDao.deleteAll(site.localId()) + } + response.result.forEach { insertTaxRateToDatabase(it, site) } + + val canLoadMore = response.result.size == pageSize + WooResult(canLoadMore) + } + else -> WooResult(WooError(GENERIC_ERROR, UNKNOWN)) } } } + + @ExperimentalCoroutinesApi + fun observeTaxRates(site: SiteModel): Flow> = + taxRateDao.observeTaxRates(site.localId()).distinctUntilChanged() + + private suspend fun insertTaxRateToDatabase(dto: TaxRateDto, site: SiteModel) { + taxRateDao.insertOrUpdate(dto.toDataModel(site.localId())) + } + + suspend fun getTaxRate(site: SiteModel, taxRateId: Long) = + taxRateDao.getTaxRate(site.localId(), RemoteId(taxRateId)) }