Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.google.gson.JsonObject
import kotlinx.parcelize.Parcelize
import org.wordpress.android.fluxc.model.WCProductModel.SubscriptionMetadataKeys
import org.wordpress.android.fluxc.model.metadata.WCMetaData
import org.wordpress.android.fluxc.model.metadata.WCMetaDataValue
import java.math.BigDecimal

@Parcelize
Expand Down Expand Up @@ -50,3 +51,15 @@ fun SubscriptionDetails.toMetadataJson(): JsonArray {
}
return jsonArray
}

fun SubscriptionDetails.toMetaData() = mapOf(
SubscriptionMetadataKeys.SUBSCRIPTION_PRICE to WCMetaDataValue(price?.toString().orEmpty()),
SubscriptionMetadataKeys.SUBSCRIPTION_PERIOD to WCMetaDataValue(period.value),
SubscriptionMetadataKeys.SUBSCRIPTION_PERIOD_INTERVAL to WCMetaDataValue(periodInterval),
SubscriptionMetadataKeys.SUBSCRIPTION_LENGTH to WCMetaDataValue(length?.toString().orEmpty()),
SubscriptionMetadataKeys.SUBSCRIPTION_SIGN_UP_FEE to WCMetaDataValue(signUpFee?.toString().orEmpty()),
SubscriptionMetadataKeys.SUBSCRIPTION_TRIAL_PERIOD to
WCMetaDataValue((trialPeriod ?: SubscriptionPeriod.Day).value),
SubscriptionMetadataKeys.SUBSCRIPTION_TRIAL_LENGTH to WCMetaDataValue(trialLength?.toString().orEmpty()),
SubscriptionMetadataKeys.SUBSCRIPTION_ONE_TIME_SHIPPING to WCMetaDataValue(if (oneTimeShipping) "yes" else "no"),
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.woocommerce.android.ui.products
import com.woocommerce.android.R
import com.woocommerce.android.WooException
import com.woocommerce.android.model.Product
import com.woocommerce.android.model.ProductAggregate
import com.woocommerce.android.ui.products.details.ProductDetailRepository
import com.woocommerce.android.ui.products.variations.VariationRepository
import com.woocommerce.android.util.WooLog
Expand All @@ -18,19 +19,24 @@ class DuplicateProduct @Inject constructor(
private val resourceProvider: ResourceProvider,
) {

suspend operator fun invoke(product: Product): Result<Long> {
val newProduct = product.copy(
remoteId = 0,
name = resourceProvider.getString(R.string.product_duplicate_copied_product_name, product.name),
sku = "",
status = ProductStatus.DRAFT
suspend operator fun invoke(productAggregate: ProductAggregate): Result<Long> {
val newProduct = productAggregate.copy(
product = productAggregate.product.copy(
remoteId = 0,
name = resourceProvider.getString(
R.string.product_duplicate_copied_product_name,
productAggregate.product.name
),
sku = "",
status = ProductStatus.DRAFT
)
)

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

return if (duplicateProductSuccess) {
if (product.numVariations > 0) {
duplicateVariations(product, duplicatedProductRemoteId)
if (productAggregate.product.numVariations > 0) {
duplicateVariations(productAggregate.product, duplicatedProductRemoteId)
} else {
Result.success(duplicatedProductRemoteId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.woocommerce.android.model.SubscriptionDetailsMapper
import com.woocommerce.android.model.TaxClass
import com.woocommerce.android.model.toAppModel
import com.woocommerce.android.model.toDataModel
import com.woocommerce.android.model.toMetaData
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.products.models.QuantityRules
import com.woocommerce.android.util.ContinuationWrapper
Expand All @@ -37,6 +38,7 @@ import org.wordpress.android.fluxc.action.WCProductAction.FETCH_SINGLE_PRODUCT_S
import org.wordpress.android.fluxc.action.WCProductAction.UPDATED_PRODUCT
import org.wordpress.android.fluxc.action.WCProductAction.UPDATE_PRODUCT_PASSWORD
import org.wordpress.android.fluxc.generated.WCProductActionBuilder
import org.wordpress.android.fluxc.model.metadata.MetadataChanges
import org.wordpress.android.fluxc.model.metadata.WCMetaData
import org.wordpress.android.fluxc.store.WCGlobalAttributeStore
import org.wordpress.android.fluxc.store.WCProductStore
Expand Down Expand Up @@ -120,14 +122,25 @@ class ProductDetailRepository @Inject constructor(
*
* @return the result of the action as a [Boolean]
*/
suspend fun updateProduct(updatedProduct: Product): Pair<Boolean, WCProductStore.ProductError?> {
suspend fun updateProduct(updatedProductAggregate: ProductAggregate): Pair<Boolean, WCProductStore.ProductError?> {
return try {
suspendCoroutineWithTimeout<Pair<Boolean, WCProductStore.ProductError?>>(AppConstants.REQUEST_TIMEOUT) {
continuationUpdateProduct = it

val cachedProduct = getCachedWCProductModel(updatedProduct.remoteId)
val product = updatedProduct.toDataModel(cachedProduct)
val payload = WCProductStore.UpdateProductPayload(selectedSite.get(), product)
val cachedProduct = getCachedWCProductModel(updatedProductAggregate.remoteId)
val product = updatedProductAggregate.product.toDataModel(cachedProduct)
val metadataChanges = MetadataChanges(
// Even though the subscription keys are passed as new metadata here, the server will replace any
// existing keys with the new ones.
insertedMetadata = updatedProductAggregate.subscription?.toMetaData()?.map { (key, value) ->
WCMetaData(id = 0L, key = key, value = value)
} ?: emptyList()
)
val payload = WCProductStore.UpdateProductPayload(
site = selectedSite.get(),
product = product,
metadataChanges = metadataChanges
)
dispatcher.dispatch(WCProductActionBuilder.newUpdateProductAction(payload))
} ?: Pair(false, null) // request timed out
} catch (e: CancellationException) {
Expand All @@ -136,17 +149,30 @@ class ProductDetailRepository @Inject constructor(
}
}

/**
* Fires the request to update the product
*
* @return the result of the action as a [Boolean]
*/
suspend fun updateProduct(updatedProduct: Product): Pair<Boolean, WCProductStore.ProductError?> {
return updateProduct(ProductAggregate(updatedProduct, null))
}

/**
* Fires the request to add a product
*
* @return the result of the action as a [Boolean]
*/
suspend fun addProduct(product: Product): Pair<Boolean, Long> {
suspend fun addProduct(productAggregate: ProductAggregate): Pair<Boolean, Long> {
return try {
suspendCoroutineWithTimeout<Pair<Boolean, Long>>(AppConstants.REQUEST_TIMEOUT) {
continuationAddProduct = it
val model = product.toDataModel(null)
val payload = WCProductStore.AddProductPayload(selectedSite.get(), model)
val model = productAggregate.product.toDataModel(null)
val payload = WCProductStore.AddProductPayload(
site = selectedSite.get(),
product = model,
metadata = productAggregate.subscription?.toMetaData()
)
dispatcher.dispatch(WCProductActionBuilder.newAddProductAction(payload))
} ?: Pair(false, 0L) // request timed out
} catch (e: CancellationException) {
Expand All @@ -155,6 +181,13 @@ class ProductDetailRepository @Inject constructor(
}
}

/**
* Fires the request to add a product
*
* @return the result of the action as a [Boolean]
*/
suspend fun addProduct(product: Product): Pair<Boolean, Long> = addProduct(ProductAggregate(product, null))

/**
* Fires the request to update the product password
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ class ProductDetailViewModel @Inject constructor(
* Validates if the view model was started for the **add** flow AND there is an already valid product to modify.
*/
val isProductUnderCreation: Boolean
get() = isAddNewProductFlow and isProductStoredAtSite.not()
get() = isAddNewProductFlow && isProductStoredAtSite.not()

/**
* Returns boolean value of [navArgs.isTrashEnabled] to determine if the detail fragment should enable
Expand Down Expand Up @@ -1017,11 +1017,11 @@ class ProductDetailViewModel @Inject constructor(
* 3. is a Draft
*/
fun saveAsDraftIfNewVariableProduct() = launch {
viewState.productDraft
viewState.productAggregateDraft
?.takeIf {
isProductStoredAtSite.not() and
it.productType.isVariableProduct() and
(it.status == DRAFT)
isProductStoredAtSite.not() &&
it.product.productType.isVariableProduct() &&
(it.product.status == DRAFT)
}
?.takeIf { addProduct(it).first }
?.let {
Expand All @@ -1035,31 +1035,31 @@ class ProductDetailViewModel @Inject constructor(
stat = AnalyticsEvent.PRODUCT_DETAIL_UPDATE_BUTTON_TAPPED,
properties = mapOf(AnalyticsTracker.KEY_IS_AI_CONTENT to navArgs.isAIContent)
)
viewState.productDraft?.let {
val product = if (isPublish) it.copy(status = ProductStatus.PUBLISH) else it
viewState.productAggregateDraft?.let {
val product = if (isPublish) it.copy(product = it.product.copy(status = ProductStatus.PUBLISH)) else it
viewState = viewState.copy(isProgressDialogShown = true)
launch { updateProduct(isPublish, product) }
}
}

private fun startPublishProduct(productStatus: ProductStatus, exitWhenDone: Boolean = false) {
viewState.productDraft?.let {
val product = it.copy(status = productStatus)
trackPublishing(product)
viewState.productAggregateDraft?.let {
val productAggregate = it.copy(product = it.product.copy(status = productStatus))
trackPublishing(productAggregate.product)

viewState = viewState.copy(isProgressDialogShown = true)

launch {
val (isSuccess, newProductId) = addProduct(product)
val (isSuccess, newProductId) = addProduct(productAggregate)
viewState = viewState.copy(isProgressDialogShown = false)
val snackbarMessage = pickAddProductRequestSnackbarText(isSuccess, productStatus)
triggerEvent(ShowSnackbar(snackbarMessage))
if (isSuccess) {
if (isPublishingFirstProduct()) {
triggerEvent(
ProductNavigationTarget.ViewFirstProductCelebration(
productName = product.name,
permalink = product.permalink
productName = productAggregate.product.name,
permalink = productAggregate.product.permalink
)
)
}
Expand All @@ -1070,13 +1070,13 @@ class ProductDetailViewModel @Inject constructor(
)
}
tracker.track(AnalyticsEvent.ADD_PRODUCT_SUCCESS)
if (product.remoteId != newProductId) {
if (productAggregate.remoteId != newProductId) {
// Assign the current uploads to the new product id
mediaFileUploadHandler.assignUploadsToCreatedProduct(newProductId)
}
if (exitWhenDone) {
triggerEvent(ProductNavigationTarget.ExitProduct)
} else if (product.remoteId != newProductId) {
} else if (productAggregate.remoteId != newProductId) {
// Restart observing image uploads using the new product id
observeImageUploadEvents()
}
Expand Down Expand Up @@ -1960,19 +1960,21 @@ class ProductDetailViewModel @Inject constructor(
* Updates the product to the backend only if network is connected.
* Otherwise, an offline snackbar is displayed.
*/
private suspend fun updateProduct(isPublish: Boolean, product: Product) {
private suspend fun updateProduct(isPublish: Boolean, productAggregate: ProductAggregate) {
if (!checkConnection()) {
viewState = viewState.copy(isProgressDialogShown = false)
return
}
val result = productRepository.updateProduct(product.copy(password = viewState.draftPassword))
val result = productRepository.updateProduct(
productAggregate.copy(product = productAggregate.product.copy(password = viewState.draftPassword))
)
if (result.first) {
val successMsg = pickProductUpdateSuccessText(isPublish)
val isPasswordChanged = storedProductAggregate.value?.product?.password != viewState.draftPassword
if (isPasswordChanged && determineProductPasswordApi() == ProductPasswordApi.WPCOM) {
// Update the product password using WordPress.com API
val password = viewState.productDraft?.password
if (productRepository.updateProductPassword(product.remoteId, password)) {
if (productRepository.updateProductPassword(productAggregate.remoteId, password)) {
storedProductAggregate.update { it?.copy(product = it.product.copy(password = password)) }
triggerEvent(ShowSnackbar(successMsg))
} else {
Expand All @@ -1988,7 +1990,7 @@ class ProductDetailViewModel @Inject constructor(
productDraft = null
)
triggerEvent(ProductUpdated)
loadRemoteProduct(product.remoteId)
loadRemoteProduct(productAggregate.remoteId)
} else {
result.second?.let {
if (it.canDisplayMessage) {
Expand All @@ -2007,10 +2009,10 @@ class ProductDetailViewModel @Inject constructor(
* Otherwise, an offline snackbar is displayed. Returns true only
* if product successfully added
*/
private suspend fun addProduct(product: Product): Pair<Boolean, Long> {
private suspend fun addProduct(productAggregate: ProductAggregate): Pair<Boolean, Long> {
if (!checkConnection()) return Pair(false, 0L)

val result = productRepository.addProduct(product)
val result = productRepository.addProduct(productAggregate)
val (isSuccess, newProductRemoteId) = result
if (isSuccess) {
checkLinkedProductPromo()
Expand Down Expand Up @@ -2518,7 +2520,7 @@ class ProductDetailViewModel @Inject constructor(
fun onDuplicateProduct() {
launch {
tracker.track(AnalyticsEvent.PRODUCT_DETAIL_DUPLICATE_BUTTON_TAPPED)
viewState.productDraft?.let { product ->
viewState.productAggregateDraft?.let { product ->

triggerEvent(ShowDuplicateProductInProgress)
val result = duplicateProduct(product)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.woocommerce.android.media.ProductImagesUploadWorker.Event.ProductUpda
import com.woocommerce.android.media.ProductImagesUploadWorker.Event.ProductUpdateEvent.ProductUpdateSucceeded
import com.woocommerce.android.media.ProductImagesUploadWorker.Event.ProductUploadsCompleted
import com.woocommerce.android.media.ProductImagesUploadWorker.Work
import com.woocommerce.android.model.Product
import com.woocommerce.android.model.toAppModel
import com.woocommerce.android.ui.products.ProductTestUtils
import com.woocommerce.android.ui.products.details.ProductDetailRepository
Expand Down Expand Up @@ -222,7 +223,7 @@ class ProductImagesUploadWorkerTest : BaseUnitTest() {
fun `when update product succeeds, then send an event`() = testBlocking {
val product = ProductTestUtils.generateProduct(REMOTE_PRODUCT_ID)
whenever(productDetailRepository.fetchAndGetProduct(REMOTE_PRODUCT_ID)).thenReturn(product)
whenever(productDetailRepository.updateProduct(any())).thenReturn(Pair(true, null))
whenever(productDetailRepository.updateProduct(any<Product>())).thenReturn(Pair(true, null))

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

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

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

val eventsList = mutableListOf<Event>()
val job = launch {
Expand Down
Loading
Loading