diff --git a/example/src/main/java/org/wordpress/android/fluxc/example/ui/metadata/CustomFieldsViewModel.kt b/example/src/main/java/org/wordpress/android/fluxc/example/ui/metadata/CustomFieldsViewModel.kt index eb99f0f14e..840e9e0855 100644 --- a/example/src/main/java/org/wordpress/android/fluxc/example/ui/metadata/CustomFieldsViewModel.kt +++ b/example/src/main/java/org/wordpress/android/fluxc/example/ui/metadata/CustomFieldsViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.metadata.MetaDataParentItemType +import org.wordpress.android.fluxc.model.metadata.MetadataChanges import org.wordpress.android.fluxc.model.metadata.UpdateMetadataRequest import org.wordpress.android.fluxc.model.metadata.WCMetaData import org.wordpress.android.fluxc.store.MetaDataStore @@ -43,6 +44,7 @@ class CustomFieldsViewModel( loadCustomFields() } + @Suppress("LongMethod") private fun observeLoadingState() { val customFields = combine( metaDataStore.observeDisplayableMetaData( @@ -63,8 +65,8 @@ class CustomFieldsViewModel( customFields, pendingUpdateRequest.map { it.insertedMetadata.isNotEmpty() || - it.updatedMetadata.isNotEmpty() || - it.deletedMetadataIds.isNotEmpty() + it.updatedMetadata.isNotEmpty() || + it.deletedMetadataIds.isNotEmpty() } ) { loadingState, metaData, hasChanges -> when (loadingState) { @@ -74,21 +76,27 @@ class CustomFieldsViewModel( onDelete = { field -> pendingUpdateRequest.update { it.copy( - deletedMetadataIds = it.deletedMetadataIds + field.id + metadataChanges = it.metadataChanges.copy( + deletedMetadataIds = it.deletedMetadataIds + field.id + ) ) } }, onEdit = { field -> pendingUpdateRequest.update { it.copy( - updatedMetadata = it.updatedMetadata + field + metadataChanges = it.metadataChanges.copy( + updatedMetadata = it.updatedMetadata + field + ) ) } }, onAdd = { field -> pendingUpdateRequest.update { it.copy( - insertedMetadata = it.insertedMetadata + field + metadataChanges = it.metadataChanges.copy( + insertedMetadata = it.insertedMetadata + field + ) ) } }, @@ -134,9 +142,7 @@ class CustomFieldsViewModel( } else { pendingUpdateRequest.update { it.copy( - insertedMetadata = emptyList(), - updatedMetadata = emptyList(), - deletedMetadataIds = emptyList() + metadataChanges = MetadataChanges() ) } loadingState.value = LoadingState.Loaded diff --git a/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductDtoMapperTest.kt b/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductDtoMapperTest.kt index bcf13e6e1f..1cc9c786a8 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductDtoMapperTest.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductDtoMapperTest.kt @@ -1,6 +1,5 @@ package org.wordpress.android.fluxc.network.rest.wpcom.wc.product -import com.google.gson.Gson import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.mockito.kotlin.any @@ -8,17 +7,16 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.wordpress.android.fluxc.JsonLoaderUtils.jsonFileAs import org.wordpress.android.fluxc.model.LocalOrRemoteId -import org.wordpress.android.fluxc.model.metadata.WCMetaData import org.wordpress.android.fluxc.model.addons.RemoteAddonDto import org.wordpress.android.fluxc.model.addons.RemoteAddonDto.RemotePriceType.FlatFee import org.wordpress.android.fluxc.model.addons.RemoteAddonDto.RemoteRestrictionsType.AnyText import org.wordpress.android.fluxc.model.addons.RemoteAddonDto.RemoteType.Checkbox +import org.wordpress.android.fluxc.model.metadata.WCMetaData import org.wordpress.android.fluxc.model.metadata.get import kotlin.test.fail class ProductDtoMapperTest { private val productDtoMapper = ProductDtoMapper( - gson = Gson(), stripProductMetaData = mock { on { invoke(any()) } doAnswer { @Suppress("UNCHECKED_CAST") @@ -79,18 +77,6 @@ class ProductDtoMapperTest { assertThat(addonOptions).isNotEmpty } - @Test - fun `Product metadata is serialized correctly`() { - val productModelUnderTest = - "wc/product-with-addons.json" - .jsonFileAs(ProductApiResponse::class.java) - ?.let { productDtoMapper.mapToModel(LocalOrRemoteId.LocalId(0), it) } - ?.product - - assertThat(productModelUnderTest).isNotNull - assertThat(productModelUnderTest?.metadata).isNotNull - } - @Test fun `Bundled product with max size is serialized correctly`() { val product = "wc/product-bundle-with-max-quantity.json" diff --git a/example/src/test/java/org/wordpress/android/fluxc/wc/leaderboards/WCLeaderboardsTestFixtures.kt b/example/src/test/java/org/wordpress/android/fluxc/wc/leaderboards/WCLeaderboardsTestFixtures.kt index b0be003fe4..9fb84f2dc7 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/wc/leaderboards/WCLeaderboardsTestFixtures.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/wc/leaderboards/WCLeaderboardsTestFixtures.kt @@ -19,7 +19,6 @@ object WCLeaderboardsTestFixtures { origin = SiteModel.ORIGIN_XMLRPC } private val productDtoMapper = ProductDtoMapper( - gson = Gson(), stripProductMetaData = mock { on { invoke(any()) } doAnswer { @Suppress("UNCHECKED_CAST") diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt index 0551b617e4..bf9c1eaaaf 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt @@ -41,7 +41,7 @@ open class WellSqlConfig : DefaultWellConfig { annotation class AddOn override fun getDbVersion(): Int { - return 204 + return 205 } override fun getDbName(): String { @@ -2053,6 +2053,66 @@ open class WellSqlConfig : DefaultWellConfig { ) """.trimIndent()) } + + 204 -> migrateAddOn(ADDON_WOOCOMMERCE, version) { + // Drop the column METADATA from WCProductModel + // Since SQLite version varies across devices, we can't rely on the support of DROP COLUMN, + // So we'll use an intermediate table + db.execSQL(""" + CREATE TABLE WCProductModel_temp ( + _id INTEGER PRIMARY KEY AUTOINCREMENT,LOCAL_SITE_ID INTEGER, + REMOTE_PRODUCT_ID INTEGER,NAME TEXT NOT NULL,SLUG TEXT NOT NULL,PERMALINK TEXT NOT NULL, + DATE_CREATED TEXT NOT NULL,DATE_MODIFIED TEXT NOT NULL,TYPE TEXT NOT NULL,STATUS TEXT NOT NULL, + FEATURED INTEGER,CATALOG_VISIBILITY TEXT NOT NULL,DESCRIPTION TEXT NOT NULL, + SHORT_DESCRIPTION TEXT NOT NULL,SKU TEXT NOT NULL,PRICE TEXT NOT NULL,REGULAR_PRICE TEXT NOT NULL, + SALE_PRICE TEXT NOT NULL,ON_SALE INTEGER,TOTAL_SALES INTEGER,PURCHASABLE INTEGER, + DATE_ON_SALE_FROM TEXT NOT NULL,DATE_ON_SALE_TO TEXT NOT NULL,DATE_ON_SALE_FROM_GMT TEXT NOT NULL, + DATE_ON_SALE_TO_GMT TEXT NOT NULL,VIRTUAL INTEGER,DOWNLOADABLE INTEGER,DOWNLOAD_LIMIT INTEGER, + DOWNLOAD_EXPIRY INTEGER,SOLD_INDIVIDUALLY INTEGER,EXTERNAL_URL TEXT NOT NULL,BUTTON_TEXT TEXT NOT NULL, + TAX_STATUS TEXT NOT NULL,TAX_CLASS TEXT NOT NULL,MANAGE_STOCK INTEGER,STOCK_QUANTITY REAL, + STOCK_STATUS TEXT NOT NULL,BACKORDERS TEXT NOT NULL,BACKORDERS_ALLOWED INTEGER,BACKORDERED INTEGER, + SHIPPING_REQUIRED INTEGER,SHIPPING_TAXABLE INTEGER,SHIPPING_CLASS TEXT NOT NULL,SHIPPING_CLASS_ID INTEGER, + REVIEWS_ALLOWED INTEGER,AVERAGE_RATING TEXT NOT NULL,RATING_COUNT INTEGER,PARENT_ID INTEGER, + PURCHASE_NOTE TEXT NOT NULL,MENU_ORDER INTEGER,CATEGORIES TEXT NOT NULL,TAGS TEXT NOT NULL, + IMAGES TEXT NOT NULL,ATTRIBUTES TEXT NOT NULL,VARIATIONS TEXT NOT NULL,DOWNLOADS TEXT NOT NULL, + RELATED_IDS TEXT NOT NULL,CROSS_SELL_IDS TEXT NOT NULL,UPSELL_IDS TEXT NOT NULL, + GROUPED_PRODUCT_IDS TEXT NOT NULL,WEIGHT TEXT NOT NULL,LENGTH TEXT NOT NULL,WIDTH TEXT NOT NULL, + HEIGHT TEXT NOT NULL,BUNDLED_ITEMS TEXT NOT NULL,COMPOSITE_COMPONENTS TEXT NOT NULL, + SPECIAL_STOCK_STATUS TEXT NOT NULL,BUNDLE_MIN_SIZE REAL,BUNDLE_MAX_SIZE REAL, + MIN_ALLOWED_QUANTITY INTEGER,MAX_ALLOWED_QUANTITY INTEGER,GROUP_OF_QUANTITY INTEGER, + COMBINE_VARIATION_QUANTITIES INTEGER,PASSWORD TEXT,IS_SAMPLE_PRODUCT INTEGER + ) + """.trimIndent()) + + db.execSQL(""" + INSERT INTO WCProductModel_temp ( + _id, LOCAL_SITE_ID, REMOTE_PRODUCT_ID, NAME, SLUG, PERMALINK, DATE_CREATED, DATE_MODIFIED, TYPE, STATUS, FEATURED, + CATALOG_VISIBILITY, DESCRIPTION, SHORT_DESCRIPTION, SKU, PRICE, REGULAR_PRICE, SALE_PRICE, ON_SALE, TOTAL_SALES, + PURCHASABLE, DATE_ON_SALE_FROM, DATE_ON_SALE_TO, DATE_ON_SALE_FROM_GMT, DATE_ON_SALE_TO_GMT, VIRTUAL, DOWNLOADABLE, + DOWNLOAD_LIMIT, DOWNLOAD_EXPIRY, SOLD_INDIVIDUALLY, EXTERNAL_URL, BUTTON_TEXT, TAX_STATUS, TAX_CLASS, MANAGE_STOCK, + STOCK_QUANTITY, STOCK_STATUS, BACKORDERS, BACKORDERS_ALLOWED, BACKORDERED, SHIPPING_REQUIRED, SHIPPING_TAXABLE, + SHIPPING_CLASS, SHIPPING_CLASS_ID, REVIEWS_ALLOWED, AVERAGE_RATING, RATING_COUNT, PARENT_ID, PURCHASE_NOTE, MENU_ORDER, + CATEGORIES, TAGS, IMAGES, ATTRIBUTES, VARIATIONS, DOWNLOADS, RELATED_IDS, CROSS_SELL_IDS, UPSELL_IDS, GROUPED_PRODUCT_IDS, + WEIGHT, LENGTH, WIDTH, HEIGHT, BUNDLED_ITEMS, COMPOSITE_COMPONENTS, SPECIAL_STOCK_STATUS, BUNDLE_MIN_SIZE, BUNDLE_MAX_SIZE, + MIN_ALLOWED_QUANTITY, MAX_ALLOWED_QUANTITY, GROUP_OF_QUANTITY, COMBINE_VARIATION_QUANTITIES, PASSWORD, IS_SAMPLE_PRODUCT + ) + SELECT + _id, LOCAL_SITE_ID, REMOTE_PRODUCT_ID, NAME, SLUG, PERMALINK, DATE_CREATED, DATE_MODIFIED, TYPE, STATUS, FEATURED, + CATALOG_VISIBILITY, DESCRIPTION, SHORT_DESCRIPTION, SKU, PRICE, REGULAR_PRICE, SALE_PRICE, ON_SALE, TOTAL_SALES, + PURCHASABLE, DATE_ON_SALE_FROM, DATE_ON_SALE_TO, DATE_ON_SALE_FROM_GMT, DATE_ON_SALE_TO_GMT, VIRTUAL, DOWNLOADABLE, + DOWNLOAD_LIMIT, DOWNLOAD_EXPIRY, SOLD_INDIVIDUALLY, EXTERNAL_URL, BUTTON_TEXT, TAX_STATUS, TAX_CLASS, MANAGE_STOCK, + STOCK_QUANTITY, STOCK_STATUS, BACKORDERS, BACKORDERS_ALLOWED, BACKORDERED, SHIPPING_REQUIRED, SHIPPING_TAXABLE, + SHIPPING_CLASS, SHIPPING_CLASS_ID, REVIEWS_ALLOWED, AVERAGE_RATING, RATING_COUNT, PARENT_ID, PURCHASE_NOTE, MENU_ORDER, + CATEGORIES, TAGS, IMAGES, ATTRIBUTES, VARIATIONS, DOWNLOADS, RELATED_IDS, CROSS_SELL_IDS, UPSELL_IDS, GROUPED_PRODUCT_IDS, + WEIGHT, LENGTH, WIDTH, HEIGHT, BUNDLED_ITEMS, COMPOSITE_COMPONENTS, SPECIAL_STOCK_STATUS, BUNDLE_MIN_SIZE, BUNDLE_MAX_SIZE, + MIN_ALLOWED_QUANTITY, MAX_ALLOWED_QUANTITY, GROUP_OF_QUANTITY, COMBINE_VARIATION_QUANTITIES, PASSWORD, IS_SAMPLE_PRODUCT + FROM WCProductModel + """.trimIndent()) + + db.execSQL("DROP TABLE WCProductModel") + + db.execSQL("ALTER TABLE WCProductModel_temp RENAME TO WCProductModel") + } } } db.setTransactionSuccessful() 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 d65b55477f..429b3ddf43 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 @@ -106,11 +106,6 @@ data class WCProductModel(@PrimaryKey @Column private var id: Int = 0) : Identif @Column var width = "" @Column var height = "" - /** - * This holds just the subscription keys, for the rest of product's metadata please check [ProductWithMetaData] - */ - @Column var metadata = "" - @Column var bundledItems = "" @Column var compositeComponents = "" @Column var specialStockStatus = "" diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/MetadataChanges.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/MetadataChanges.kt new file mode 100644 index 0000000000..c44226c11e --- /dev/null +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/MetadataChanges.kt @@ -0,0 +1,41 @@ +package org.wordpress.android.fluxc.model.metadata + +import com.google.gson.JsonArray +import com.google.gson.JsonNull +import com.google.gson.JsonObject + +data class MetadataChanges( + val insertedMetadata: List = emptyList(), + val updatedMetadata: List = emptyList(), + val deletedMetadataIds: List = emptyList(), +) { + init { + // The ID of inserted metadata is ignored, so to ensure that there is no data loss here, + // we require that all inserted metadata have an ID of 0. + require(insertedMetadata.all { it.id == 0L }) { + "Inserted metadata must have an ID of 0" + } + } + + internal fun toJsonArray() = JsonArray().apply { + insertedMetadata.forEach { + add( + JsonObject().apply { + addProperty(WCMetaData.KEY, it.key) + add(WCMetaData.VALUE, it.value.jsonValue) + } + ) + } + updatedMetadata.forEach { + add(it.toJson()) + } + deletedMetadataIds.forEach { + add( + JsonObject().apply { + addProperty(WCMetaData.ID, it) + add(WCMetaData.VALUE, JsonNull.INSTANCE) + } + ) + } + } +} diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/UpdateMetadataRequest.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/UpdateMetadataRequest.kt index d5acad3ec2..49465af3d0 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/UpdateMetadataRequest.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/UpdateMetadataRequest.kt @@ -3,15 +3,26 @@ package org.wordpress.android.fluxc.model.metadata data class UpdateMetadataRequest( val parentItemId: Long, val parentItemType: MetaDataParentItemType, - val insertedMetadata: List = emptyList(), - val updatedMetadata: List = emptyList(), - val deletedMetadataIds: List = emptyList(), + val metadataChanges: MetadataChanges, ) { - init { - // The ID of inserted metadata is ignored, so to ensure that there is no data loss here, - // we require that all inserted metadata have an ID of 0. - require(insertedMetadata.all { it.id == 0L }) { - "Inserted metadata must have an ID of 0" - } - } + val insertedMetadata: List get() = metadataChanges.insertedMetadata + val updatedMetadata: List get() = metadataChanges.updatedMetadata + val deletedMetadataIds: List get() = metadataChanges.deletedMetadataIds + + constructor( + parentItemId: Long, + parentItemType: MetaDataParentItemType, + insertedMetadata: List = emptyList(), + updatedMetadata: List = emptyList(), + deletedMetadataIds: List = emptyList(), + ) : this( + parentItemId, + parentItemType, + MetadataChanges( + insertedMetadata, + updatedMetadata, + deletedMetadataIds, + ) + ) } + diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/WCMetaData.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/WCMetaData.kt index 64135560a3..9981281db9 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/WCMetaData.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/WCMetaData.kt @@ -25,7 +25,7 @@ data class WCMetaData( val displayValue: WCMetaDataValue? = null ) { constructor(id: Long, key: String, value: String) : this(id, key, - WCMetaDataValue.fromRawString(value) + WCMetaDataValue(value) ) /** diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/WCMetaDataValue.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/WCMetaDataValue.kt index cce8c27c11..e51354fd01 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/WCMetaDataValue.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/metadata/WCMetaDataValue.kt @@ -78,6 +78,16 @@ sealed class WCMetaDataValue { } companion object { + operator fun invoke(value: String?): WCMetaDataValue { + if (value == null) return StringValue(null) + + return runCatching { JsonParser().parse(value) } + .getOrElse { JsonPrimitive(value) } + .let { fromJsonElement(it) } + } + operator fun invoke(value: Number): WCMetaDataValue = NumberValue(value) + operator fun invoke(value: Boolean): WCMetaDataValue = BooleanValue(value) + internal fun fromJsonElement(element: JsonElement): WCMetaDataValue { return when { element.isJsonPrimitive -> { @@ -95,10 +105,5 @@ sealed class WCMetaDataValue { else -> StringValue(element.toString()) } } - - fun fromRawString(value: String): WCMetaDataValue = - runCatching { JsonParser().parse(value) } - .getOrElse { JsonPrimitive(value) } - .let { fromJsonElement(it) } } } diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/metadata/MetaDataRestClient.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/metadata/MetaDataRestClient.kt index 687f5a97fc..831fe68b9a 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/metadata/MetaDataRestClient.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/metadata/MetaDataRestClient.kt @@ -1,7 +1,5 @@ package org.wordpress.android.fluxc.network.rest.wpcom.wc.metadata -import com.google.gson.JsonArray -import com.google.gson.JsonNull import com.google.gson.JsonObject import org.wordpress.android.fluxc.generated.endpoint.WOOCOMMERCE import org.wordpress.android.fluxc.model.SiteModel @@ -47,32 +45,11 @@ internal class MetaDataRestClient @Inject internal constructor( MetaDataParentItemType.PRODUCT -> WOOCOMMERCE.products.id(request.parentItemId).pathV3 } - val metaDataJson = JsonArray() - request.insertedMetadata.forEach { - metaDataJson.add( - JsonObject().apply { - addProperty(WCMetaData.KEY, it.key) - add(WCMetaData.VALUE, it.value.jsonValue) - } - ) - } - request.updatedMetadata.forEach { - metaDataJson.add(it.toJson()) - } - request.deletedMetadataIds.forEach { - metaDataJson.add( - JsonObject().apply { - addProperty(WCMetaData.ID, it) - add(WCMetaData.VALUE, JsonNull.INSTANCE) - } - ) - } - val response = wooNetwork.executePostGsonRequest( site = site, path = path, body = mapOf( - "meta_data" to metaDataJson, + "meta_data" to request.metadataChanges.toJsonArray(), "_fields" to "meta_data" ), clazz = JsonObject::class.java diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductDtoMapper.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductDtoMapper.kt index c22e4130e5..2c2ccbdff7 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductDtoMapper.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductDtoMapper.kt @@ -1,16 +1,14 @@ package org.wordpress.android.fluxc.network.rest.wpcom.wc.product -import com.google.gson.Gson import org.wordpress.android.fluxc.model.LocalOrRemoteId import org.wordpress.android.fluxc.model.ProductWithMetaData import org.wordpress.android.fluxc.model.StripProductMetaData -import org.wordpress.android.fluxc.model.metadata.WCMetaData import org.wordpress.android.fluxc.model.WCProductModel +import org.wordpress.android.fluxc.model.metadata.WCMetaData import org.wordpress.android.fluxc.network.utils.getString import javax.inject.Inject class ProductDtoMapper @Inject constructor( - private val gson: Gson, private val stripProductMetaData: StripProductMetaData ) { @Suppress("LongMethod", "ComplexMethod") @@ -109,10 +107,6 @@ class ProductDtoMapper @Inject constructor( it == "yes" } ?: false - // Save only the subscription data, the rest of the metadata will be saved separately - metadata = metaData.filter { it.key in WCProductModel.SubscriptionMetadataKeys.ALL_KEYS } - .let { gson.toJson(it) } - password = dto.password isSampleProduct = dto.metadata?.any { 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 81dda78e73..24dcb528e9 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 @@ -1,6 +1,7 @@ package org.wordpress.android.fluxc.network.rest.wpcom.wc.product import com.google.gson.JsonArray +import com.google.gson.JsonObject import com.google.gson.JsonParser import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.action.WCProductAction @@ -19,6 +20,9 @@ import org.wordpress.android.fluxc.model.WCProductReviewModel import org.wordpress.android.fluxc.model.WCProductShippingClassModel import org.wordpress.android.fluxc.model.WCProductTagModel import org.wordpress.android.fluxc.model.WCProductVariationModel +import org.wordpress.android.fluxc.model.metadata.MetadataChanges +import org.wordpress.android.fluxc.model.metadata.WCMetaData +import org.wordpress.android.fluxc.model.metadata.WCMetaDataValue import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType.PARSE_ERROR import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError @@ -935,16 +939,22 @@ class ProductRestClient @Inject constructor( * @param [site] The site to fetch product reviews for * @param [storedWCProductModel] the stored model to compare with the [updatedProductModel] * @param [updatedProductModel] the product model that contains the update + * @param [metadataChanges] the metadata changes to be updated */ fun updateProduct( site: SiteModel, storedWCProductModel: WCProductModel?, - updatedProductModel: WCProductModel + updatedProductModel: WCProductModel, + metadataChanges: MetadataChanges? = null ) { coroutineEngine.launch(AppLog.T.API, this, "updateProduct") { val remoteProductId = updatedProductModel.remoteProductId val url = WOOCOMMERCE.products.id(remoteProductId).pathV3 - val body = productModelToProductJsonBody(storedWCProductModel, updatedProductModel) + val body = productModelToProductJsonBody( + productModel = storedWCProductModel, + updatedProductModel = updatedProductModel, + metadata = metadataChanges?.toJsonArray() + ) val response = wooNetwork.executePutGsonRequest( site = site, @@ -1710,14 +1720,29 @@ class ProductRestClient @Inject constructor( * * @param [site] The site to fetch product reviews for * @param [productModel] the new product model + * @param [metaData] the metadata to be added to the product */ fun addProduct( site: SiteModel, - productModel: WCProductModel + productModel: WCProductModel, + metaData: Map? = null ) { coroutineEngine.launch(AppLog.T.API, this, "addProduct") { val url = WOOCOMMERCE.products.pathV3 - val body = productModelToProductJsonBody(null, productModel) + val body = productModelToProductJsonBody( + productModel = null, + updatedProductModel = productModel, + metadata = metaData?.let { + JsonArray().apply { + it.forEach { (key, value) -> + add(JsonObject().apply { + addProperty(WCMetaData.KEY, key) + add(WCMetaData.VALUE, value.jsonValue) + }) + } + } + } + ) val response = wooNetwork.executePostGsonRequest( site = site, @@ -1805,7 +1830,8 @@ class ProductRestClient @Inject constructor( @Suppress("LongMethod", "ComplexMethod", "SwallowedException", "TooGenericExceptionCaught") private fun productModelToProductJsonBody( productModel: WCProductModel?, - updatedProductModel: WCProductModel + updatedProductModel: WCProductModel, + metadata: JsonArray? = null ): HashMap { val body = HashMap() @@ -1989,15 +2015,8 @@ class ProductRestClient @Inject constructor( } } } - if (storedWCProductModel.metadata != updatedProductModel.metadata) { - JsonParser().apply { - body["meta_data"] = try { - parse(updatedProductModel.metadata).asJsonArray - } catch (ex: Exception) { - AppLog.e(AppLog.T.API, "Error parsing product metadata", ex) - JsonArray() - } - } + if (metadata != null) { + body["meta_data"] = metadata } if (storedWCProductModel.password != updatedProductModel.password) { body["post_password"] = updatedProductModel.password.orEmpty() 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 49f1df6c5d..906cdbb6c3 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 @@ -15,7 +15,6 @@ import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId import org.wordpress.android.fluxc.model.ProductWithMetaData import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.VariationAttributes -import org.wordpress.android.fluxc.model.metadata.WCMetaData import org.wordpress.android.fluxc.model.WCProductCategoryModel import org.wordpress.android.fluxc.model.WCProductComponent import org.wordpress.android.fluxc.model.WCProductImageModel @@ -26,6 +25,9 @@ import org.wordpress.android.fluxc.model.WCProductTagModel import org.wordpress.android.fluxc.model.WCProductVariationModel import org.wordpress.android.fluxc.model.WCProductVariationModel.ProductVariantOption import org.wordpress.android.fluxc.model.addons.RemoteAddonDto +import org.wordpress.android.fluxc.model.metadata.MetadataChanges +import org.wordpress.android.fluxc.model.metadata.WCMetaData +import org.wordpress.android.fluxc.model.metadata.WCMetaDataValue import org.wordpress.android.fluxc.model.metadata.get import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType @@ -179,7 +181,8 @@ class WCProductStore @Inject constructor( class UpdateProductPayload( var site: SiteModel, - val product: WCProductModel + val product: WCProductModel, + val metadataChanges: MetadataChanges? = null ) : Payload() class BatchUpdateProductsPayload( @@ -302,7 +305,8 @@ class WCProductStore @Inject constructor( class AddProductPayload( var site: SiteModel, - val product: WCProductModel + val product: WCProductModel, + val metadata: Map? = null ) : Payload() class DeleteProductPayload( @@ -1399,7 +1403,7 @@ class WCProductStore @Inject constructor( private fun updateProduct(payload: UpdateProductPayload) { with(payload) { val storedProduct = getProductByRemoteId(site, product.remoteProductId) - wcProductRestClient.updateProduct(site, storedProduct, product) + wcProductRestClient.updateProduct(site, storedProduct, product, payload.metadataChanges) } } @@ -1806,7 +1810,7 @@ class WCProductStore @Inject constructor( private fun addProduct(payload: AddProductPayload) { with(payload) { - wcProductRestClient.addProduct(site, product) + wcProductRestClient.addProduct(site, product, payload.metadata) } } diff --git a/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/model/WCMetaDataValueTest.kt b/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/model/WCMetaDataValueTest.kt index 0a04373d3a..3b37b611ac 100644 --- a/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/model/WCMetaDataValueTest.kt +++ b/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/model/WCMetaDataValueTest.kt @@ -6,37 +6,37 @@ import org.wordpress.android.fluxc.model.metadata.WCMetaDataValue class WCMetaDataValueTest { @Test fun `when given a plain string, should return a StringValue`() { - val string = WCMetaDataValue.fromRawString("string") + val string = WCMetaDataValue("string") assert(string is WCMetaDataValue.StringValue) } @Test fun `when given a number, should return a NumberValue`() { - val number = WCMetaDataValue.fromRawString("1") + val number = WCMetaDataValue("1") assert(number is WCMetaDataValue.NumberValue) } @Test fun `when given a float, should return a NumberValue`() { - val number = WCMetaDataValue.fromRawString("1.5") + val number = WCMetaDataValue("1.5") assert(number is WCMetaDataValue.NumberValue) } @Test fun `when given a boolean, should return a BooleanValue`() { - val boolean = WCMetaDataValue.fromRawString("true") + val boolean = WCMetaDataValue("true") assert(boolean is WCMetaDataValue.BooleanValue) } @Test fun `when given a JSON object, should return a JsonObjectValue`() { - val jsonObject = WCMetaDataValue.fromRawString("""{"key":"value"}""") + val jsonObject = WCMetaDataValue("""{"key":"value"}""") assert(jsonObject is WCMetaDataValue.JsonObjectValue) } @Test fun `when given a JSON array, should return a JsonArrayValue`() { - val jsonArray = WCMetaDataValue.fromRawString("""[{"key":"value"}]""") + val jsonArray = WCMetaDataValue("""[{"key":"value"}]""") assert(jsonArray is WCMetaDataValue.JsonArrayValue) } }