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
@@ -1,7 +1,5 @@
package com.woocommerce.android.ui.woopos.common.data

import com.woocommerce.android.model.ProductVariation
import com.woocommerce.android.model.toAppModel
import com.woocommerce.android.tools.SelectedSite
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
Expand All @@ -11,8 +9,10 @@ import javax.inject.Inject
class WooPosGetVariationById @Inject constructor(
private val store: WCProductStore,
private val site: SelectedSite,
private val mapper: WooPosVariationMapper,
) {
suspend operator fun invoke(productId: Long, variationId: Long): ProductVariation? = withContext(IO) {
store.getVariationByRemoteId(site.getOrNull()!!, productId, variationId)?.toAppModel()
suspend operator fun invoke(productId: Long, variationId: Long): WooPosVariation? = withContext(IO) {
val siteModel = site.getOrNull() ?: return@withContext null
store.getVariationByRemoteId(siteModel, productId, variationId)?.toWooPosVariation(mapper)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.woocommerce.android.ui.woopos.common.data

import java.math.BigDecimal

/**
* POS-specific product variation model containing only the fields needed for POS functionality.
* This model provides better performance and cleaner separation compared to the general ProductVariation model.
*/
data class WooPosVariation(
val remoteVariationId: Long,
val remoteProductId: Long,
val globalUniqueId: String,
val price: BigDecimal?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 This is technically enough for now, but we'll likely need to have access to both sale and regular prices quite soon, so I'd consider including them similarly to how we do it in the product model. Having said that, feel free to leave it as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave these props for now and update the model when we need them.

val image: WooPosVariationImage?,
val attributes: List<WooPosVariationAttribute>,
val isVisible: Boolean,
val isDownloadable: Boolean
) {

data class WooPosVariationImage(
val source: String
)

data class WooPosVariationAttribute(
val id: Long?,
val name: String?,
val option: String?
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.woocommerce.android.ui.woopos.common.data

import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import com.woocommerce.android.R
import com.woocommerce.android.model.Product
import com.woocommerce.android.model.ProductVariation
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.viewmodel.ResourceProvider
import org.wordpress.android.fluxc.model.WCProductVariationModel
import org.wordpress.android.fluxc.persistence.entity.pos.WCPosVariationModel
import javax.inject.Inject
import javax.inject.Singleton

/**
* Mapper class responsible for all WooPosVariation conversions and transformations.
* This centralizes all variation mapping logic, JSON parsing, and name generation.
*/
@Singleton
class WooPosVariationMapper @Inject constructor(
private val gson: Gson
) {
fun fromProductVariation(productVariation: ProductVariation): WooPosVariation {
return WooPosVariation(
remoteVariationId = productVariation.remoteVariationId,
remoteProductId = productVariation.remoteProductId,
globalUniqueId = productVariation.globalUniqueId,
price = productVariation.price,
image = productVariation.image?.let { WooPosVariation.WooPosVariationImage(it.source) },
attributes = productVariation.attributes.map {
WooPosVariation.WooPosVariationAttribute(
id = it.id,
name = it.name,
option = it.option
)
},
isVisible = productVariation.isVisible,
isDownloadable = productVariation.isDownloadable
)
}

@Suppress("SwallowedException")
fun fromWCProductVariationModel(model: WCProductVariationModel): WooPosVariation {
val attributesList = model.attributeList?.map { attribute ->
WooPosVariation.WooPosVariationAttribute(
id = attribute.id,
name = attribute.name,
option = attribute.option
)
} ?: emptyList()

val imageModel = try {
if (model.image.isNotEmpty()) model.getImageModel() else null
} catch (e: JsonSyntaxException) {
WooLog.w(
WooLog.T.POS,
"Failed to parse image from JSON attributes for variation " +
"${model.remoteVariationId.value}: ${e.message}"
)
null
}

return WooPosVariation(
remoteVariationId = model.remoteVariationId.value,
remoteProductId = model.remoteProductId.value,
globalUniqueId = model.globalUniqueId,
price = model.price.toBigDecimalOrNull(),
image = imageModel?.src?.let { WooPosVariation.WooPosVariationImage(it) },
attributes = attributesList,
isVisible = model.status == "publish",
isDownloadable = model.downloadable
)
}

@Suppress("SwallowedException")
fun fromWCPosVariationModel(model: WCPosVariationModel): WooPosVariation {
val attributesList = parseAttributesJson(model.attributesJson)

return WooPosVariation(
remoteVariationId = model.remoteVariationId.value,
remoteProductId = model.remoteProductId.value,
globalUniqueId = model.globalUniqueId,
price = model.price.toBigDecimalOrNull(),
image = if (model.imageUrl.isNotEmpty()) WooPosVariation.WooPosVariationImage(model.imageUrl) else null,
attributes = attributesList,
isVisible = model.status == "publish",
isDownloadable = model.downloadable
)
}

fun getNameForPOS(
variation: WooPosVariation,
parentProduct: Product? = null,
resourceProvider: ResourceProvider,
): String {
return parentProduct?.variationEnabledAttributes?.joinToString(", ") { attribute ->
val option = variation.attributes.firstOrNull { it.name == attribute.name }
if (option?.option != null) {
"${attribute.name}: ${option.option}"
} else {
resourceProvider.getString(
R.string.woopos_variations_any_variation,
attribute.name
)
}
} ?: variation.attributes.joinToString(", ") { attribute ->
when {
attribute.option != null && attribute.name != null -> "${attribute.name}: ${attribute.option}"
attribute.option != null -> attribute.option
attribute.name != null -> resourceProvider.getString(
R.string.woopos_variations_any_variation,
attribute.name
)
else -> ""
}
}
}

fun getName(variation: WooPosVariation, parentProduct: Product? = null): String {
return parentProduct?.variationEnabledAttributes?.joinToString(" - ") { attribute ->
val option = variation.attributes.firstOrNull { it.name == attribute.name }
option?.option ?: "Any ${attribute.name}"
} ?: variation.attributes.mapNotNull { it.option }.joinToString(" - ")
}

/**
* Parses JSON string containing variation attributes into a list of WooPosVariationAttribute objects.
*
* @param attributesJson The JSON string to parse
* @return List of parsed attributes, or empty list if parsing fails
*/
private fun parseAttributesJson(attributesJson: String): List<WooPosVariation.WooPosVariationAttribute> {
if (attributesJson.isEmpty()) return emptyList()

return try {
val type = object : TypeToken<List<AttributeJsonItem>>() {}.type
val items: List<AttributeJsonItem> = gson.fromJson(attributesJson, type)
items.map { item ->
WooPosVariation.WooPosVariationAttribute(
id = item.id,
name = item.name,
option = item.option
)
}
} catch (e: JsonSyntaxException) {
WooLog.w(WooLog.T.POS, "Failed to parse attributes JSON: ${e.message}")
emptyList()
}
}

private data class AttributeJsonItem(
val id: Long?,
val name: String?,
val option: String?
)
}

fun ProductVariation.toWooPosVariation(mapper: WooPosVariationMapper): WooPosVariation =
mapper.fromProductVariation(this)

fun WCProductVariationModel.toWooPosVariation(mapper: WooPosVariationMapper): WooPosVariation =
mapper.fromWCProductVariationModel(this)

fun WCPosVariationModel.toWooPosVariation(mapper: WooPosVariationMapper): WooPosVariation =
mapper.fromWCPosVariationModel(this)

fun WooPosVariation.getNameForPOS(
mapper: WooPosVariationMapper,
parentProduct: Product? = null,
resourceProvider: ResourceProvider,
): String = mapper.getNameForPOS(this, parentProduct, resourceProvider)

fun WooPosVariation.getName(mapper: WooPosVariationMapper, parentProduct: Product? = null): String =
mapper.getName(this, parentProduct)
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.woocommerce.android.ui.woopos.common.data.searchbyidentifier

import com.woocommerce.android.model.Product
import com.woocommerce.android.model.ProductVariation
import com.woocommerce.android.ui.woopos.common.data.WooPosProductsTypesFilterConfig
import com.woocommerce.android.ui.woopos.common.data.WooPosVariation
import com.woocommerce.android.ui.woopos.common.data.WooPosVariationsTypesFilterConfig
import com.woocommerce.android.ui.woopos.common.util.WooPosLogWrapper
import org.wordpress.android.fluxc.store.WCProductStore
Expand Down Expand Up @@ -75,7 +75,7 @@ class WooPosSearchByIdentifier @Inject constructor(
}
}

private fun meetsVariationFilterRequirements(variation: ProductVariation): Boolean {
private fun meetsVariationFilterRequirements(variation: WooPosVariation): Boolean {
val requiredStatus = variationFilterConfig.filters[VariationFilterOption.STATUS]
val hasValidStatus = when (requiredStatus) {
WooPosVariationsTypesFilterConfig.VARIATION_STATUS_PUBLISH -> variation.isVisible
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.woocommerce.android.ui.woopos.common.data.searchbyidentifier

import com.woocommerce.android.model.Product
import com.woocommerce.android.model.ProductVariation
import com.woocommerce.android.ui.woopos.common.data.WooPosVariation

sealed class WooPosSearchByIdentifierResult {
data class Success(val product: Product) : WooPosSearchByIdentifierResult()
data class VariationSuccess(val variation: ProductVariation, val parentProduct: Product) :
data class VariationSuccess(val variation: WooPosVariation, val parentProduct: Product) :
WooPosSearchByIdentifierResult()

data class Failure(val error: Error) : WooPosSearchByIdentifierResult()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.woocommerce.android.ui.woopos.common.data.searchbyidentifier

import com.woocommerce.android.model.ProductVariation
import com.woocommerce.android.model.toAppModel
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.woopos.common.data.WooPosVariation
import com.woocommerce.android.ui.woopos.common.data.WooPosVariationMapper
import com.woocommerce.android.ui.woopos.common.data.toWooPosVariation
import com.woocommerce.android.ui.woopos.home.items.variations.WooPosVariationsLRUCache
import org.wordpress.android.fluxc.store.WCProductStore
import javax.inject.Inject
Expand All @@ -11,10 +12,11 @@ class WooPosSearchByIdentifierVariationFetch @Inject constructor(
private val selectedSite: SelectedSite,
private val productStore: WCProductStore,
private val variationsCache: WooPosVariationsLRUCache,
private val errorMapper: WooPosSearchByIdentifierProductErrorMapper
private val errorMapper: WooPosSearchByIdentifierProductErrorMapper,
private val mapper: WooPosVariationMapper
) {
sealed class VariationFetchResult {
data class Success(val variation: ProductVariation) : VariationFetchResult()
data class Success(val variation: WooPosVariation) : VariationFetchResult()
data class Failure(val error: WooPosSearchByIdentifierResult.Error) : VariationFetchResult()
}

Expand All @@ -34,7 +36,7 @@ class WooPosSearchByIdentifierVariationFetch @Inject constructor(
selectedSite.get(),
parentId,
variationId
)?.toAppModel()
)?.toWooPosVariation(mapper)

return if (variation != null) {
variationsCache.add(parentId, variation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.woocommerce.android.R
import com.woocommerce.android.model.Product
import com.woocommerce.android.model.ProductVariation
import com.woocommerce.android.ui.woopos.common.composeui.modifier.BarcodeInputDetector
import com.woocommerce.android.ui.woopos.common.data.WooPosGetCouponById
import com.woocommerce.android.ui.woopos.common.data.WooPosGetProductById
import com.woocommerce.android.ui.woopos.common.data.WooPosGetVariationById
import com.woocommerce.android.ui.woopos.common.data.WooPosVariation
import com.woocommerce.android.ui.woopos.common.data.WooPosVariationMapper
import com.woocommerce.android.ui.woopos.common.data.getNameForPOS
import com.woocommerce.android.ui.woopos.common.data.searchbyidentifier.WooPosSearchByIdentifier
import com.woocommerce.android.ui.woopos.common.data.searchbyidentifier.WooPosSearchByIdentifierResult
import com.woocommerce.android.ui.woopos.common.util.WooPosLogWrapper
Expand All @@ -28,7 +30,6 @@ import com.woocommerce.android.ui.woopos.home.cart.WooPosCartStatus.CHECKOUT
import com.woocommerce.android.ui.woopos.home.cart.WooPosCartStatus.EDITABLE
import com.woocommerce.android.ui.woopos.home.cart.WooPosCartStatus.EMPTY
import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel
import com.woocommerce.android.ui.woopos.home.items.variations.getNameForPOS
import com.woocommerce.android.ui.woopos.util.WooPosGetCachedStoreCurrency
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.BackToCartTapped
Expand Down Expand Up @@ -69,6 +70,7 @@ class WooPosCartViewModel @Inject constructor(
private val wooPosLogWrapper: WooPosLogWrapper,
private val soundHelper: WooPosSoundHelper,
private val barcodeEventTracker: WooPosBarcodeEventTracker,
private val variationMapper: WooPosVariationMapper,
savedState: SavedStateHandle,
) : ViewModel() {
private val _state = savedState.getStateFlow(
Expand Down Expand Up @@ -589,7 +591,7 @@ class WooPosCartViewModel @Inject constructor(
imageUrl = firstImageUrl,
)

private suspend fun ProductVariation.toCartListItem(
private suspend fun WooPosVariation.toCartListItem(
itemNumber: Int,
product: Product
): WooPosCartItemViewState.Product.Variation =
Expand All @@ -598,7 +600,7 @@ class WooPosCartViewModel @Inject constructor(
id = product.remoteId,
variationId = this.remoteVariationId,
name = product.name,
description = getNameForPOS(product, resourceProvider),
description = getNameForPOS(variationMapper, product, resourceProvider),
price = formatPrice(price),
imageUrl = image?.source,
)
Expand Down Expand Up @@ -635,7 +637,7 @@ class WooPosCartViewModel @Inject constructor(
id = variation.remoteProductId,
variationId = variation.remoteVariationId,
name = this.parentProduct.name,
description = variation.getNameForPOS(this.parentProduct, resourceProvider),
description = variation.getNameForPOS(variationMapper, this.parentProduct, resourceProvider),
price = formatPrice(variation.price),
imageUrl = variation.image?.source
)
Expand Down
Loading