Skip to content

Commit daca67d

Browse files
Merge pull request #12767 from woocommerce/issue/12752-subscriptions-meta-2
[Products] Extract subscriptions outside of the Product DB table - Part 2
2 parents d1751be + f7e6c34 commit daca67d

File tree

9 files changed

+127
-69
lines changed

9 files changed

+127
-69
lines changed

WooCommerce/src/main/kotlin/com/woocommerce/android/model/SubscriptionDetails.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.google.gson.JsonObject
66
import kotlinx.parcelize.Parcelize
77
import org.wordpress.android.fluxc.model.WCProductModel.SubscriptionMetadataKeys
88
import org.wordpress.android.fluxc.model.metadata.WCMetaData
9+
import org.wordpress.android.fluxc.model.metadata.WCMetaDataValue
910
import java.math.BigDecimal
1011

1112
@Parcelize
@@ -50,3 +51,15 @@ fun SubscriptionDetails.toMetadataJson(): JsonArray {
5051
}
5152
return jsonArray
5253
}
54+
55+
fun SubscriptionDetails.toMetaData() = mapOf(
56+
SubscriptionMetadataKeys.SUBSCRIPTION_PRICE to WCMetaDataValue(price?.toString().orEmpty()),
57+
SubscriptionMetadataKeys.SUBSCRIPTION_PERIOD to WCMetaDataValue(period.value),
58+
SubscriptionMetadataKeys.SUBSCRIPTION_PERIOD_INTERVAL to WCMetaDataValue(periodInterval),
59+
SubscriptionMetadataKeys.SUBSCRIPTION_LENGTH to WCMetaDataValue(length?.toString().orEmpty()),
60+
SubscriptionMetadataKeys.SUBSCRIPTION_SIGN_UP_FEE to WCMetaDataValue(signUpFee?.toString().orEmpty()),
61+
SubscriptionMetadataKeys.SUBSCRIPTION_TRIAL_PERIOD to
62+
WCMetaDataValue((trialPeriod ?: SubscriptionPeriod.Day).value),
63+
SubscriptionMetadataKeys.SUBSCRIPTION_TRIAL_LENGTH to WCMetaDataValue(trialLength?.toString().orEmpty()),
64+
SubscriptionMetadataKeys.SUBSCRIPTION_ONE_TIME_SHIPPING to WCMetaDataValue(if (oneTimeShipping) "yes" else "no"),
65+
)

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/DuplicateProduct.kt

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.woocommerce.android.ui.products
33
import com.woocommerce.android.R
44
import com.woocommerce.android.WooException
55
import com.woocommerce.android.model.Product
6+
import com.woocommerce.android.model.ProductAggregate
67
import com.woocommerce.android.ui.products.details.ProductDetailRepository
78
import com.woocommerce.android.ui.products.variations.VariationRepository
89
import com.woocommerce.android.util.WooLog
@@ -18,19 +19,24 @@ class DuplicateProduct @Inject constructor(
1819
private val resourceProvider: ResourceProvider,
1920
) {
2021

21-
suspend operator fun invoke(product: Product): Result<Long> {
22-
val newProduct = product.copy(
23-
remoteId = 0,
24-
name = resourceProvider.getString(R.string.product_duplicate_copied_product_name, product.name),
25-
sku = "",
26-
status = ProductStatus.DRAFT
22+
suspend operator fun invoke(productAggregate: ProductAggregate): Result<Long> {
23+
val newProduct = productAggregate.copy(
24+
product = productAggregate.product.copy(
25+
remoteId = 0,
26+
name = resourceProvider.getString(
27+
R.string.product_duplicate_copied_product_name,
28+
productAggregate.product.name
29+
),
30+
sku = "",
31+
status = ProductStatus.DRAFT
32+
)
2733
)
2834

2935
val (duplicateProductSuccess, duplicatedProductRemoteId) = productDetailRepository.addProduct(newProduct)
3036

3137
return if (duplicateProductSuccess) {
32-
if (product.numVariations > 0) {
33-
duplicateVariations(product, duplicatedProductRemoteId)
38+
if (productAggregate.product.numVariations > 0) {
39+
duplicateVariations(productAggregate.product, duplicatedProductRemoteId)
3440
} else {
3541
Result.success(duplicatedProductRemoteId)
3642
}

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/details/ProductDetailRepository.kt

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.woocommerce.android.model.SubscriptionDetailsMapper
1515
import com.woocommerce.android.model.TaxClass
1616
import com.woocommerce.android.model.toAppModel
1717
import com.woocommerce.android.model.toDataModel
18+
import com.woocommerce.android.model.toMetaData
1819
import com.woocommerce.android.tools.SelectedSite
1920
import com.woocommerce.android.ui.products.models.QuantityRules
2021
import com.woocommerce.android.util.ContinuationWrapper
@@ -37,6 +38,7 @@ import org.wordpress.android.fluxc.action.WCProductAction.FETCH_SINGLE_PRODUCT_S
3738
import org.wordpress.android.fluxc.action.WCProductAction.UPDATED_PRODUCT
3839
import org.wordpress.android.fluxc.action.WCProductAction.UPDATE_PRODUCT_PASSWORD
3940
import org.wordpress.android.fluxc.generated.WCProductActionBuilder
41+
import org.wordpress.android.fluxc.model.metadata.MetadataChanges
4042
import org.wordpress.android.fluxc.model.metadata.WCMetaData
4143
import org.wordpress.android.fluxc.store.WCGlobalAttributeStore
4244
import org.wordpress.android.fluxc.store.WCProductStore
@@ -120,14 +122,25 @@ class ProductDetailRepository @Inject constructor(
120122
*
121123
* @return the result of the action as a [Boolean]
122124
*/
123-
suspend fun updateProduct(updatedProduct: Product): Pair<Boolean, WCProductStore.ProductError?> {
125+
suspend fun updateProduct(updatedProductAggregate: ProductAggregate): Pair<Boolean, WCProductStore.ProductError?> {
124126
return try {
125127
suspendCoroutineWithTimeout<Pair<Boolean, WCProductStore.ProductError?>>(AppConstants.REQUEST_TIMEOUT) {
126128
continuationUpdateProduct = it
127129

128-
val cachedProduct = getCachedWCProductModel(updatedProduct.remoteId)
129-
val product = updatedProduct.toDataModel(cachedProduct)
130-
val payload = WCProductStore.UpdateProductPayload(selectedSite.get(), product)
130+
val cachedProduct = getCachedWCProductModel(updatedProductAggregate.remoteId)
131+
val product = updatedProductAggregate.product.toDataModel(cachedProduct)
132+
val metadataChanges = MetadataChanges(
133+
// Even though the subscription keys are passed as new metadata here, the server will replace any
134+
// existing keys with the new ones.
135+
insertedMetadata = updatedProductAggregate.subscription?.toMetaData()?.map { (key, value) ->
136+
WCMetaData(id = 0L, key = key, value = value)
137+
} ?: emptyList()
138+
)
139+
val payload = WCProductStore.UpdateProductPayload(
140+
site = selectedSite.get(),
141+
product = product,
142+
metadataChanges = metadataChanges
143+
)
131144
dispatcher.dispatch(WCProductActionBuilder.newUpdateProductAction(payload))
132145
} ?: Pair(false, null) // request timed out
133146
} catch (e: CancellationException) {
@@ -136,17 +149,30 @@ class ProductDetailRepository @Inject constructor(
136149
}
137150
}
138151

152+
/**
153+
* Fires the request to update the product
154+
*
155+
* @return the result of the action as a [Boolean]
156+
*/
157+
suspend fun updateProduct(updatedProduct: Product): Pair<Boolean, WCProductStore.ProductError?> {
158+
return updateProduct(ProductAggregate(updatedProduct, null))
159+
}
160+
139161
/**
140162
* Fires the request to add a product
141163
*
142164
* @return the result of the action as a [Boolean]
143165
*/
144-
suspend fun addProduct(product: Product): Pair<Boolean, Long> {
166+
suspend fun addProduct(productAggregate: ProductAggregate): Pair<Boolean, Long> {
145167
return try {
146168
suspendCoroutineWithTimeout<Pair<Boolean, Long>>(AppConstants.REQUEST_TIMEOUT) {
147169
continuationAddProduct = it
148-
val model = product.toDataModel(null)
149-
val payload = WCProductStore.AddProductPayload(selectedSite.get(), model)
170+
val model = productAggregate.product.toDataModel(null)
171+
val payload = WCProductStore.AddProductPayload(
172+
site = selectedSite.get(),
173+
product = model,
174+
metadata = productAggregate.subscription?.toMetaData()
175+
)
150176
dispatcher.dispatch(WCProductActionBuilder.newAddProductAction(payload))
151177
} ?: Pair(false, 0L) // request timed out
152178
} catch (e: CancellationException) {
@@ -155,6 +181,13 @@ class ProductDetailRepository @Inject constructor(
155181
}
156182
}
157183

184+
/**
185+
* Fires the request to add a product
186+
*
187+
* @return the result of the action as a [Boolean]
188+
*/
189+
suspend fun addProduct(product: Product): Pair<Boolean, Long> = addProduct(ProductAggregate(product, null))
190+
158191
/**
159192
* Fires the request to update the product password
160193
*

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/details/ProductDetailViewModel.kt

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ class ProductDetailViewModel @Inject constructor(
376376
* Validates if the view model was started for the **add** flow AND there is an already valid product to modify.
377377
*/
378378
val isProductUnderCreation: Boolean
379-
get() = isAddNewProductFlow and isProductStoredAtSite.not()
379+
get() = isAddNewProductFlow && isProductStoredAtSite.not()
380380

381381
/**
382382
* Returns boolean value of [navArgs.isTrashEnabled] to determine if the detail fragment should enable
@@ -1017,11 +1017,11 @@ class ProductDetailViewModel @Inject constructor(
10171017
* 3. is a Draft
10181018
*/
10191019
fun saveAsDraftIfNewVariableProduct() = launch {
1020-
viewState.productDraft
1020+
viewState.productAggregateDraft
10211021
?.takeIf {
1022-
isProductStoredAtSite.not() and
1023-
it.productType.isVariableProduct() and
1024-
(it.status == DRAFT)
1022+
isProductStoredAtSite.not() &&
1023+
it.product.productType.isVariableProduct() &&
1024+
(it.product.status == DRAFT)
10251025
}
10261026
?.takeIf { addProduct(it).first }
10271027
?.let {
@@ -1035,31 +1035,31 @@ class ProductDetailViewModel @Inject constructor(
10351035
stat = AnalyticsEvent.PRODUCT_DETAIL_UPDATE_BUTTON_TAPPED,
10361036
properties = mapOf(AnalyticsTracker.KEY_IS_AI_CONTENT to navArgs.isAIContent)
10371037
)
1038-
viewState.productDraft?.let {
1039-
val product = if (isPublish) it.copy(status = ProductStatus.PUBLISH) else it
1038+
viewState.productAggregateDraft?.let {
1039+
val product = if (isPublish) it.copy(product = it.product.copy(status = ProductStatus.PUBLISH)) else it
10401040
viewState = viewState.copy(isProgressDialogShown = true)
10411041
launch { updateProduct(isPublish, product) }
10421042
}
10431043
}
10441044

10451045
private fun startPublishProduct(productStatus: ProductStatus, exitWhenDone: Boolean = false) {
1046-
viewState.productDraft?.let {
1047-
val product = it.copy(status = productStatus)
1048-
trackPublishing(product)
1046+
viewState.productAggregateDraft?.let {
1047+
val productAggregate = it.copy(product = it.product.copy(status = productStatus))
1048+
trackPublishing(productAggregate.product)
10491049

10501050
viewState = viewState.copy(isProgressDialogShown = true)
10511051

10521052
launch {
1053-
val (isSuccess, newProductId) = addProduct(product)
1053+
val (isSuccess, newProductId) = addProduct(productAggregate)
10541054
viewState = viewState.copy(isProgressDialogShown = false)
10551055
val snackbarMessage = pickAddProductRequestSnackbarText(isSuccess, productStatus)
10561056
triggerEvent(ShowSnackbar(snackbarMessage))
10571057
if (isSuccess) {
10581058
if (isPublishingFirstProduct()) {
10591059
triggerEvent(
10601060
ProductNavigationTarget.ViewFirstProductCelebration(
1061-
productName = product.name,
1062-
permalink = product.permalink
1061+
productName = productAggregate.product.name,
1062+
permalink = productAggregate.product.permalink
10631063
)
10641064
)
10651065
}
@@ -1070,13 +1070,13 @@ class ProductDetailViewModel @Inject constructor(
10701070
)
10711071
}
10721072
tracker.track(AnalyticsEvent.ADD_PRODUCT_SUCCESS)
1073-
if (product.remoteId != newProductId) {
1073+
if (productAggregate.remoteId != newProductId) {
10741074
// Assign the current uploads to the new product id
10751075
mediaFileUploadHandler.assignUploadsToCreatedProduct(newProductId)
10761076
}
10771077
if (exitWhenDone) {
10781078
triggerEvent(ProductNavigationTarget.ExitProduct)
1079-
} else if (product.remoteId != newProductId) {
1079+
} else if (productAggregate.remoteId != newProductId) {
10801080
// Restart observing image uploads using the new product id
10811081
observeImageUploadEvents()
10821082
}
@@ -1954,19 +1954,21 @@ class ProductDetailViewModel @Inject constructor(
19541954
* Updates the product to the backend only if network is connected.
19551955
* Otherwise, an offline snackbar is displayed.
19561956
*/
1957-
private suspend fun updateProduct(isPublish: Boolean, product: Product) {
1957+
private suspend fun updateProduct(isPublish: Boolean, productAggregate: ProductAggregate) {
19581958
if (!checkConnection()) {
19591959
viewState = viewState.copy(isProgressDialogShown = false)
19601960
return
19611961
}
1962-
val result = productRepository.updateProduct(product.copy(password = viewState.draftPassword))
1962+
val result = productRepository.updateProduct(
1963+
productAggregate.copy(product = productAggregate.product.copy(password = viewState.draftPassword))
1964+
)
19631965
if (result.first) {
19641966
val successMsg = pickProductUpdateSuccessText(isPublish)
19651967
val isPasswordChanged = storedProductAggregate.value?.product?.password != viewState.draftPassword
19661968
if (isPasswordChanged && determineProductPasswordApi() == ProductPasswordApi.WPCOM) {
19671969
// Update the product password using WordPress.com API
19681970
val password = viewState.productDraft?.password
1969-
if (productRepository.updateProductPassword(product.remoteId, password)) {
1971+
if (productRepository.updateProductPassword(productAggregate.remoteId, password)) {
19701972
storedProductAggregate.update { it?.copy(product = it.product.copy(password = password)) }
19711973
triggerEvent(ShowSnackbar(successMsg))
19721974
} else {
@@ -1982,7 +1984,7 @@ class ProductDetailViewModel @Inject constructor(
19821984
productDraft = null
19831985
)
19841986
triggerEvent(ProductUpdated)
1985-
loadRemoteProduct(product.remoteId)
1987+
loadRemoteProduct(productAggregate.remoteId)
19861988
} else {
19871989
result.second?.let {
19881990
if (it.canDisplayMessage) {
@@ -2001,10 +2003,10 @@ class ProductDetailViewModel @Inject constructor(
20012003
* Otherwise, an offline snackbar is displayed. Returns true only
20022004
* if product successfully added
20032005
*/
2004-
private suspend fun addProduct(product: Product): Pair<Boolean, Long> {
2006+
private suspend fun addProduct(productAggregate: ProductAggregate): Pair<Boolean, Long> {
20052007
if (!checkConnection()) return Pair(false, 0L)
20062008

2007-
val result = productRepository.addProduct(product)
2009+
val result = productRepository.addProduct(productAggregate)
20082010
val (isSuccess, newProductRemoteId) = result
20092011
if (isSuccess) {
20102012
checkLinkedProductPromo()
@@ -2512,7 +2514,7 @@ class ProductDetailViewModel @Inject constructor(
25122514
fun onDuplicateProduct() {
25132515
launch {
25142516
tracker.track(AnalyticsEvent.PRODUCT_DETAIL_DUPLICATE_BUTTON_TAPPED)
2515-
viewState.productDraft?.let { product ->
2517+
viewState.productAggregateDraft?.let { product ->
25162518

25172519
triggerEvent(ShowDuplicateProductInProgress)
25182520
val result = duplicateProduct(product)

WooCommerce/src/test/kotlin/com/woocommerce/android/media/ProductImagesUploadWorkerTest.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.woocommerce.android.media.ProductImagesUploadWorker.Event.ProductUpda
1313
import com.woocommerce.android.media.ProductImagesUploadWorker.Event.ProductUpdateEvent.ProductUpdateSucceeded
1414
import com.woocommerce.android.media.ProductImagesUploadWorker.Event.ProductUploadsCompleted
1515
import com.woocommerce.android.media.ProductImagesUploadWorker.Work
16+
import com.woocommerce.android.model.Product
1617
import com.woocommerce.android.model.toAppModel
1718
import com.woocommerce.android.ui.products.ProductTestUtils
1819
import com.woocommerce.android.ui.products.details.ProductDetailRepository
@@ -222,7 +223,7 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() {
222223
fun `when update product succeeds, then send an event`() = testBlocking {
223224
val product = ProductTestUtils.generateProduct(REMOTE_PRODUCT_ID)
224225
whenever(productDetailRepository.fetchAndGetProduct(REMOTE_PRODUCT_ID)).thenReturn(product)
225-
whenever(productDetailRepository.updateProduct(any())).thenReturn(Pair(true, null))
226+
whenever(productDetailRepository.updateProduct(any<Product>())).thenReturn(Pair(true, null))
226227

227228
val eventsList = mutableListOf<Event>()
228229
val job = launch {
@@ -239,7 +240,7 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() {
239240
fun `when update product fails, then retry three times`() = testBlocking {
240241
val product = ProductTestUtils.generateProduct(REMOTE_PRODUCT_ID)
241242
whenever(productDetailRepository.fetchAndGetProduct(REMOTE_PRODUCT_ID)).thenReturn(product)
242-
whenever(productDetailRepository.updateProduct(any())).thenReturn(Pair(false, null))
243+
whenever(productDetailRepository.updateProduct(any<Product>())).thenReturn(Pair(false, null))
243244

244245
worker.enqueueWork(Work.UpdateProduct(REMOTE_PRODUCT_ID, listOf(UPLOADED_MEDIA)))
245246

@@ -252,7 +253,7 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() {
252253
fun `when update product fails, then send an event`() = testBlocking {
253254
val product = ProductTestUtils.generateProduct(REMOTE_PRODUCT_ID)
254255
whenever(productDetailRepository.fetchAndGetProduct(REMOTE_PRODUCT_ID)).thenReturn(product)
255-
whenever(productDetailRepository.updateProduct(any())).thenReturn(Pair(false, null))
256+
whenever(productDetailRepository.updateProduct(any<Product>())).thenReturn(Pair(false, null))
256257

257258
val eventsList = mutableListOf<Event>()
258259
val job = launch {

0 commit comments

Comments
 (0)