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 new file mode 100644 index 0000000000..4b2bd0a217 --- /dev/null +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/StripProductMetaData.kt @@ -0,0 +1,33 @@ +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.AddOnsMetadataKeys +import org.wordpress.android.fluxc.model.WCProductModel.QuantityRulesMetadataKeys +import org.wordpress.android.fluxc.utils.EMPTY_JSON_ARRAY +import org.wordpress.android.fluxc.utils.isElementNullOrEmpty +import javax.inject.Inject + +class StripProductMetaData @Inject internal constructor(private val gson: Gson) { + operator fun invoke(metadata: String?): String { + if (metadata.isNullOrEmpty()) return EMPTY_JSON_ARRAY + + return gson.fromJson(metadata, JsonArray::class.java) + .mapNotNull { it as? JsonObject } + .asSequence() + .filter { jsonObject -> + val isNullOrEmpty = jsonObject[WCMetaData.VALUE].isElementNullOrEmpty() + jsonObject[WCMetaData.KEY]?.asString.orEmpty() in SUPPORTED_KEYS && isNullOrEmpty.not() + }.toList() + .takeIf { it.isNotEmpty() } + ?.let { gson.toJson(it) } ?: EMPTY_JSON_ARRAY + } + + companion object { + val SUPPORTED_KEYS: Set = buildSet { + add(AddOnsMetadataKeys.ADDONS_METADATA_KEY) + addAll(QuantityRulesMetadataKeys.ALL_KEYS) + } + } +} diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/StripProductVariationMetaData.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/StripProductVariationMetaData.kt index 6c404ccf24..c85beb1e37 100644 --- a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/StripProductVariationMetaData.kt +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/model/StripProductVariationMetaData.kt @@ -3,18 +3,19 @@ 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.utils.isElementNullOrEmpty import javax.inject.Inject class StripProductVariationMetaData @Inject internal constructor(private val gson: Gson) { operator fun invoke(metadata: String?): String? { - if (metadata == null) return null + if (metadata.isNullOrEmpty()) return null return gson.fromJson(metadata, JsonArray::class.java) .mapNotNull { it as? JsonObject } .asSequence() .filter { jsonObject -> - jsonObject[WCMetaData.KEY]?.asString.orEmpty() in SUPPORTED_KEYS && - jsonObject[WCMetaData.VALUE]?.asString.orEmpty().isNotBlank() + val isNullOrEmpty = jsonObject[WCMetaData.VALUE].isElementNullOrEmpty() + jsonObject[WCMetaData.KEY]?.asString.orEmpty() in SUPPORTED_KEYS && isNullOrEmpty.not() }.toList() .takeIf { it.isNotEmpty() } ?.let { gson.toJson(it) } 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 3887644f0f..211be93139 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 @@ -10,6 +10,7 @@ import com.yarolegovich.wellsql.core.annotation.Column import com.yarolegovich.wellsql.core.annotation.PrimaryKey import com.yarolegovich.wellsql.core.annotation.Table import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId +import org.wordpress.android.fluxc.model.WCProductModel.AddOnsMetadataKeys.ADDONS_METADATA_KEY import org.wordpress.android.fluxc.model.WCProductVariationModel.ProductVariantOption import org.wordpress.android.fluxc.model.addons.RemoteAddonDto import org.wordpress.android.fluxc.network.utils.getBoolean @@ -26,10 +27,6 @@ import org.wordpress.android.util.AppLog.T */ @Table(addOn = WellSqlConfig.ADDON_WOOCOMMERCE) data class WCProductModel(@PrimaryKey @Column private var id: Int = 0) : Identifiable { - companion object { - private const val ADDONS_METADATA_KEY = "_product_addons" - } - @Column var localSiteId = 0 @Column var remoteProductId = 0L // The unique identifier for this product on the server val remoteId @@ -557,6 +554,10 @@ data class WCProductModel(@PrimaryKey @Column private var id: Int = 0) : Identif return storedFiles == updatedFiles } + object AddOnsMetadataKeys { + const val ADDONS_METADATA_KEY = "_product_addons" + } + object QuantityRulesMetadataKeys { const val MINIMUM_ALLOWED_QUANTITY = "minimum_allowed_quantity" const val MAXIMUM_ALLOWED_QUANTITY = "maximum_allowed_quantity" 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 e9247cc1fa..7d9bd5044c 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 @@ -10,6 +10,7 @@ import org.wordpress.android.fluxc.generated.endpoint.WPAPI import org.wordpress.android.fluxc.generated.endpoint.WPCOMREST import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.StripProductMetaData import org.wordpress.android.fluxc.model.StripProductVariationMetaData import org.wordpress.android.fluxc.model.WCProductCategoryModel import org.wordpress.android.fluxc.model.WCProductImageModel @@ -82,6 +83,7 @@ class ProductRestClient @Inject constructor( private val wooNetwork: WooNetwork, private val wpComNetwork: WPComNetwork, private val coroutineEngine: CoroutineEngine, + private val stripProductMetaData: StripProductMetaData, private val stripProductVariationMetaData: StripProductVariationMetaData ) { /** @@ -291,6 +293,7 @@ class ProductRestClient @Inject constructor( response.data?.let { val newModel = it.asProductModel().apply { localSiteId = site.id + metadata = stripProductMetaData(metadata) } RemoteProductPayload(newModel, site) } ?: RemoteProductPayload( @@ -410,7 +413,10 @@ class ProductRestClient @Inject constructor( when (response) { is WPAPIResponse.Success -> { val productModels = response.data?.map { - it.asProductModel().apply { localSiteId = site.id } + it.asProductModel().apply { + localSiteId = site.id + metadata = stripProductMetaData(metadata) + } }.orEmpty() val loadedMore = offset > 0 @@ -520,7 +526,10 @@ class ProductRestClient @Inject constructor( return response.toWooPayload { products -> products.map { it.asProductModel() - .apply { localSiteId = site.id } + .apply { + localSiteId = site.id + metadata = stripProductMetaData(metadata) + } } } } @@ -876,6 +885,7 @@ class ProductRestClient @Inject constructor( response.data?.let { val newModel = it.asProductModel().apply { localSiteId = site.id + metadata = stripProductMetaData(metadata) } val payload = RemoteUpdateProductPayload(site, newModel) dispatcher.dispatch(WCProductActionBuilder.newUpdatedProductAction(payload)) @@ -1189,6 +1199,7 @@ class ProductRestClient @Inject constructor( response.data?.let { val newModel = it.asProductModel().apply { localSiteId = site.id + metadata = stripProductMetaData(metadata) } val payload = RemoteUpdateProductImagesPayload(site, newModel) dispatcher.dispatch(WCProductActionBuilder.newUpdatedProductImagesAction(payload)) @@ -1486,6 +1497,7 @@ class ProductRestClient @Inject constructor( val newModel = product.asProductModel().apply { id = product.id?.toInt() ?: 0 localSiteId = site.id + metadata = stripProductMetaData(metadata) } val payload = RemoteAddProductPayload(site, newModel) dispatcher.dispatch(WCProductActionBuilder.newAddedProductAction(payload)) diff --git a/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/utils/JsonUtils.kt b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/utils/JsonUtils.kt new file mode 100644 index 0000000000..5e4cbe68bf --- /dev/null +++ b/plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/utils/JsonUtils.kt @@ -0,0 +1,17 @@ +package org.wordpress.android.fluxc.utils + +import com.google.gson.JsonElement + +const val EMPTY_JSON_ARRAY = "[]" + +fun JsonElement?.isElementNullOrEmpty(): Boolean { + return this?.let { + when{ + this.isJsonObject -> this.asJsonObject.size() == 0 + this.isJsonArray -> this.asJsonArray.size()== 0 + this.isJsonPrimitive && this.asJsonPrimitive.isString -> this.asString.isEmpty() + this.isJsonNull -> true + else -> false + } + } ?: true +} diff --git a/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductRestClientTest.kt b/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductRestClientTest.kt index 3d33dd04c8..843db0d2cc 100644 --- a/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductRestClientTest.kt +++ b/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductRestClientTest.kt @@ -37,7 +37,7 @@ class ProductRestClientTest { private val wpComNetwork: WPComNetwork = mock() @Before fun setUp() { - sut = ProductRestClient(mock(), wooNetwork, wpComNetwork, initCoroutineEngine(), mock()) + sut = ProductRestClient(mock(), wooNetwork, wpComNetwork, initCoroutineEngine(), mock(), mock()) } @Test diff --git a/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/StripProductMetaDataTest.kt b/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/StripProductMetaDataTest.kt new file mode 100644 index 0000000000..e39884599c --- /dev/null +++ b/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/StripProductMetaDataTest.kt @@ -0,0 +1,217 @@ +package org.wordpress.android.fluxc.network.rest.wpcom.wc.product + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import org.assertj.core.api.Assertions +import org.junit.Test +import org.wordpress.android.fluxc.model.StripProductMetaData +import org.wordpress.android.fluxc.model.WCMetaData +import org.wordpress.android.fluxc.utils.EMPTY_JSON_ARRAY + +class StripProductMetaDataTest { + private val gson = Gson() + private val sut = StripProductMetaData(gson) + + + @Test + fun `when metadata contains not supported keys, then NOT supported keys are stripped`() { + val result = sut.invoke(notOnlySupportedMetadata) + val jsonResult = gson.fromJson(result, JsonArray::class.java) + + jsonResult.map { it as? JsonObject }.forEach { jsonObject -> + Assertions.assertThat(jsonObject).isNotNull + Assertions.assertThat(jsonObject?.get(WCMetaData.KEY)?.asString) + .isIn(StripProductMetaData.SUPPORTED_KEYS) + } + } + + @Test + fun `when metadata contains a supported key with a NULL value, then strip the supported key`() { + val supportedKey = StripProductMetaData.SUPPORTED_KEYS.first() + val value: String? = null + + val metadata = getOneItemMetadata(supportedKey, value) + val result = sut.invoke(metadata) + + Assertions.assertThat(result).isEqualTo(EMPTY_JSON_ARRAY) + } + + @Test + fun `when metadata contains a supported key with a EMPTY value, then strip the supported key`() { + val supportedKey = StripProductMetaData.SUPPORTED_KEYS.first() + val value = "" + + val metadata = getOneItemMetadata(supportedKey, value) + val result = sut.invoke(metadata) + + Assertions.assertThat(result).isEqualTo(EMPTY_JSON_ARRAY) + } + + @Test + fun `when metadata contains a supported key with a NOT EMPTY value, then value is kept`() { + val supportedKey = StripProductMetaData.SUPPORTED_KEYS.first() + val value = "valid" + + val metadata = getOneItemMetadata(supportedKey, value) + val result = sut.invoke(metadata) + + val jsonResult = gson.fromJson(result, JsonArray::class.java) + val resultItem = jsonResult[0] as JsonObject + Assertions.assertThat(result).isNotNull + Assertions.assertThat(resultItem[WCMetaData.KEY].asString).isEqualTo(supportedKey) + Assertions.assertThat(resultItem[WCMetaData.VALUE].asString).isEqualTo(value) + } + + @Test + fun `when metadata is null, then empty array is return`() { + val result = sut.invoke(null) + Assertions.assertThat(result).isEqualTo(EMPTY_JSON_ARRAY) + } + + private fun getOneItemMetadata(itemKey: String, itemValue: String?): String { + val item = JsonObject().apply { + addProperty(WCMetaData.KEY, itemKey) + itemValue?.let { + addProperty(WCMetaData.VALUE, it) + } ?: add(WCMetaData.VALUE, null) + } + val jsonArray = JsonArray().apply { + add(item) + } + + return gson.toJson(jsonArray) + } + + private val notOnlySupportedMetadata = """ + [ + { + "id": 11749, + "key": "_wpcom_is_markdown", + "value": "1" + }, + { + "id": 11750, + "key": "_last_editor_used_jetpack", + "value": "classic-editor" + }, + { + "id": 11754, + "key": "_product_addons", + "value": [{ + "id": 11773, + "key": "_wc_gla_sync_status", + "value": "synced" + }, + { + "id": 11774, + "key": "group_of_quantity", + "value": "" + }] + }, + { + "id": 11755, + "key": "_product_addons_exclude_global", + "value": "1" + }, + { + "id": 11772, + "key": "_wc_gla_visibility", + "value": "sync-and-show" + }, + { + "id": 11773, + "key": "_wc_gla_sync_status", + "value": "synced" + }, + { + "id": 11774, + "key": "group_of_quantity", + "value": "" + }, + { + "id": 11775, + "key": "minimum_allowed_quantity", + "value": "" + }, + { + "id": 11776, + "key": "maximum_allowed_quantity", + "value": "" + }, + { + "id": 11777, + "key": "minmax_do_not_count", + "value": "no" + }, + { + "id": 11778, + "key": "minmax_cart_exclude", + "value": "no" + }, + { + "id": 11779, + "key": "minmax_category_group_of_exclude", + "value": "no" + }, + { + "id": 11780, + "key": "_wcsatt_disabled", + "value": "yes" + }, + { + "id": 11781, + "key": "_subscription_one_time_shipping", + "value": "no" + }, + { + "id": 11782, + "key": "_wcsatt_force_subscription", + "value": "no" + }, + { + "id": 11785, + "key": "_subscription_downloads_ids", + "value": "" + }, + { + "id": 11786, + "key": "minimum_allowed_quantity", + "value": "40" + }, + { + "id": 11787, + "key": "_wc_pre_orders_fee", + "value": "" + }, + { + "id": 11788, + "key": "_wpas_done_all", + "value": "1" + }, + { + "id": 11909, + "key": "_wc_gla_mc_status", + "value": "not_synced" + }, + { + "id": 12406, + "key": "_wc_gla_synced_at", + "value": "1686193267" + }, + { + "id": 12407, + "key": "_wc_gla_google_ids", + "value": { + "US": "online:en:US:gla_418" + } + }, + { + "key": "_satt_data", + "value": { + "subscription_schemes": [] + } + } + ] + """.trimIndent() +} diff --git a/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/StripProductVariationMetaDataTest.kt b/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/StripProductVariationMetaDataTest.kt index c81a0e6cce..afb37b9bdb 100644 --- a/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/StripProductVariationMetaDataTest.kt +++ b/plugins/woocommerce/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/StripProductVariationMetaDataTest.kt @@ -61,6 +61,41 @@ class StripProductVariationMetaDataTest { assertThat(resultItem[WCMetaData.VALUE].asString).isEqualTo(value) } + @Test + fun `when metadata is null, then null is return`() { + val result = sut.invoke(null) + assertThat(result).isNull() + } + + @Test + fun `when metadata is empty, then null is return`() { + val result = sut.invoke("") + assertThat(result).isNull() + } + + @Test + fun `when metadata contains a supported key with an ARRAY value, then value is kept`() { + val supportedKey = StripProductVariationMetaData.SUPPORTED_KEYS.first() + val value = JsonArray().apply { + add("Value 1") + add("Value 2") + } + val item = JsonObject().apply { + addProperty(WCMetaData.KEY, supportedKey) + add(WCMetaData.VALUE, value) + } + val jsonArray = JsonArray().apply { add(item) } + val metadata = gson.toJson(jsonArray) + + val result = sut.invoke(metadata) + + val jsonResult = gson.fromJson(result, JsonArray::class.java) + val resultItem = jsonResult[0] as JsonObject + assertThat(result).isNotNull + assertThat(resultItem[WCMetaData.KEY].asString).isEqualTo(supportedKey) + assertThat(resultItem[WCMetaData.VALUE].asJsonArray).isEqualTo(value) + } + private fun getOneItemMetadata(itemKey: String, itemValue: String?): String { val item = JsonObject().apply { addProperty(WCMetaData.KEY, itemKey)