Skip to content

Commit 8bedab3

Browse files
authored
Merge pull request #14565 from woocommerce/issue/woomob-1243-woo-poslocal-catalog-use-new-pos-specific-product-model-in
POS Product Migration Step 1
2 parents cc78ca9 + e29723f commit 8bedab3

File tree

11 files changed

+4600
-25
lines changed

11 files changed

+4600
-25
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.woocommerce.android.ui.woopos.common.data.models
2+
3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
5+
import java.math.BigDecimal
6+
7+
/**
8+
* This model provides a clean separation between the data layer (WCPosProductEntity)
9+
* and the view layer, ensuring all required data is present and properly typed.
10+
*/
11+
@Parcelize
12+
data class WooPosProductModelVersion2(
13+
val remoteId: Long,
14+
val parentId: Long?,
15+
val name: String,
16+
val sku: String,
17+
val globalUniqueId: String,
18+
val type: WooPosProductType,
19+
val status: WooPosProductStatus,
20+
val pricing: WooPosPricing,
21+
val description: String,
22+
val shortDescription: String,
23+
val isDownloadable: Boolean,
24+
val lastModified: String,
25+
val images: List<WooPosProductImage> = emptyList(),
26+
val attributes: List<WooPosProductAttribute> = emptyList(),
27+
val categories: List<WooPosProductCategory> = emptyList(),
28+
val tags: List<WooPosProductTag> = emptyList(),
29+
) : Parcelable {
30+
31+
sealed class WooPosPricing : Parcelable {
32+
@Parcelize
33+
data object NoPricing : WooPosPricing()
34+
35+
@Parcelize
36+
data class RegularPricing(
37+
val price: BigDecimal
38+
) : WooPosPricing()
39+
40+
@Parcelize
41+
data class SalePricing(
42+
val regularPrice: BigDecimal,
43+
val salePrice: BigDecimal
44+
) : WooPosPricing()
45+
46+
val displayPrice: BigDecimal?
47+
get() = when (this) {
48+
is NoPricing -> null
49+
is RegularPricing -> price
50+
is SalePricing -> salePrice
51+
}
52+
53+
val isOnSale: Boolean
54+
get() = this is SalePricing
55+
56+
val hasPrice: Boolean
57+
get() = this != NoPricing
58+
59+
val formattedPrice: String
60+
get() = displayPrice?.toPlainString() ?: ""
61+
}
62+
63+
enum class WooPosProductType {
64+
SIMPLE,
65+
VARIABLE,
66+
GROUPED,
67+
EXTERNAL,
68+
VARIATION,
69+
SUBSCRIPTION,
70+
VARIABLE_SUBSCRIPTION,
71+
CUSTOM,
72+
BUNDLE,
73+
COMPOSITE
74+
}
75+
76+
enum class WooPosProductStatus {
77+
PUBLISH,
78+
DRAFT,
79+
PENDING,
80+
PRIVATE,
81+
TRASH,
82+
UNKNOWN
83+
}
84+
85+
@Parcelize
86+
data class WooPosProductImage(
87+
val id: Long,
88+
val url: String,
89+
val name: String,
90+
val alt: String?
91+
) : Parcelable
92+
93+
@Parcelize
94+
data class WooPosProductAttribute(
95+
val id: Long,
96+
val name: String,
97+
val options: List<String>,
98+
val isVisible: Boolean,
99+
val isVariation: Boolean
100+
) : Parcelable
101+
102+
@Parcelize
103+
data class WooPosProductCategory(
104+
val id: Long,
105+
val name: String,
106+
val slug: String
107+
) : Parcelable
108+
109+
@Parcelize
110+
data class WooPosProductTag(
111+
val id: Long,
112+
val name: String,
113+
val slug: String
114+
) : Parcelable
115+
116+
val firstImageUrl: String?
117+
get() = images.firstOrNull()?.url
118+
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package com.woocommerce.android.ui.woopos.common.data.models
2+
3+
import com.google.gson.Gson
4+
import com.google.gson.JsonSyntaxException
5+
import com.google.gson.reflect.TypeToken
6+
import com.woocommerce.android.ui.woopos.common.util.WooPosLogWrapper
7+
import dagger.Reusable
8+
import org.wordpress.android.fluxc.persistence.entity.pos.WCPosProductEntity
9+
import java.math.BigDecimal
10+
import javax.inject.Inject
11+
12+
/**
13+
* Mapper for converting WCPosProductEntity (database layer) to WooPosProductModel (domain layer).
14+
*
15+
* This is a read-only mapper since POS doesn't modify products.
16+
* The mapper ensures clean data handling:
17+
*/
18+
@Reusable
19+
class WooPosProductModelVersion2Mapper @Inject constructor(val logger: WooPosLogWrapper) {
20+
private val gson = Gson()
21+
fun fromEntity(entity: WCPosProductEntity): WooPosProductModelVersion2 {
22+
return WooPosProductModelVersion2(
23+
remoteId = entity.remoteId.value,
24+
parentId = entity.parentId,
25+
name = entity.name,
26+
sku = entity.sku,
27+
globalUniqueId = entity.globalUniqueId,
28+
type = mapProductType(entity.type),
29+
status = mapProductStatus(entity.status),
30+
pricing = mapPricing(
31+
parsePriceOrNull(entity.price),
32+
parsePriceOrNull(entity.regularPrice),
33+
parsePriceOrNull(entity.salePrice),
34+
entity.onSale
35+
),
36+
description = entity.description,
37+
shortDescription = entity.shortDescription,
38+
isDownloadable = entity.downloadable,
39+
images = parseImages(entity.images),
40+
attributes = parseAttributes(entity.attributes),
41+
categories = parseCategories(entity.categories),
42+
tags = parseTags(entity.tags),
43+
lastModified = entity.dateModified
44+
)
45+
}
46+
47+
fun fromEntities(entities: List<WCPosProductEntity>): List<WooPosProductModelVersion2> {
48+
return entities.map { fromEntity(it) }
49+
}
50+
51+
private fun parsePriceOrNull(priceString: String): BigDecimal? {
52+
return try {
53+
if (priceString.isBlank()) {
54+
null
55+
} else {
56+
BigDecimal(priceString)
57+
}
58+
} catch (e: NumberFormatException) {
59+
logger.e("Failed to parse price: '$priceString'", e)
60+
null
61+
}
62+
}
63+
64+
private fun parseImages(imagesJson: String): List<WooPosProductModelVersion2.WooPosProductImage> {
65+
return try {
66+
if (imagesJson.isBlank()) {
67+
emptyList()
68+
} else {
69+
val type = object : TypeToken<List<Map<String, Any?>>>() {}.type
70+
val imagesList: List<Map<String, Any?>> = gson.fromJson(imagesJson, type)
71+
imagesList.mapNotNull { imageMap ->
72+
return@mapNotNull parseImage(imageMap)
73+
}
74+
}
75+
} catch (e: JsonSyntaxException) {
76+
logger.w("Failed to parse images JSON: $imagesJson - ${e.message}")
77+
emptyList()
78+
}
79+
}
80+
81+
private fun parseImage(imageMap: Map<String, Any?>): WooPosProductModelVersion2.WooPosProductImage? {
82+
val id = when (val idValue = imageMap["id"]) {
83+
is Double -> idValue.toLong()
84+
is Int -> idValue.toLong()
85+
is Long -> idValue
86+
is String -> idValue.toLongOrNull()
87+
else -> null
88+
}
89+
val url = imageMap["src"] as? String
90+
val name = imageMap["name"] as? String ?: ""
91+
val alt = imageMap["alt"] as? String ?: ""
92+
if (id == null || url.isNullOrBlank()) {
93+
logger.w("Failed to parse images JSON, id or url is null: $id, $url")
94+
return null
95+
}
96+
return WooPosProductModelVersion2.WooPosProductImage(id, url, name, alt)
97+
}
98+
99+
private fun parseAttributes(attributesJson: String): List<WooPosProductModelVersion2.WooPosProductAttribute> {
100+
return try {
101+
if (attributesJson.isBlank()) {
102+
emptyList()
103+
} else {
104+
val type = object : TypeToken<List<Map<String, Any?>>>() {}.type
105+
val attributesList: List<Map<String, Any?>> = gson.fromJson(attributesJson, type)
106+
attributesList.mapNotNull { attrMap ->
107+
return@mapNotNull parseAttribute(attrMap)
108+
}
109+
}
110+
} catch (e: JsonSyntaxException) {
111+
logger.w("Failed to parse attributes JSON: $attributesJson - ${e.message}")
112+
emptyList()
113+
}
114+
}
115+
116+
private fun parseAttribute(attrMap: Map<String, Any?>): WooPosProductModelVersion2.WooPosProductAttribute? {
117+
val id = when (val idValue = attrMap["id"]) {
118+
is Double -> idValue.toLong()
119+
is Int -> idValue.toLong()
120+
is Long -> idValue
121+
is String -> idValue.toLongOrNull() ?: 0L
122+
else -> 0L
123+
}
124+
val name = attrMap["name"] as? String ?: return null
125+
val options = when (val optionsValue = attrMap["options"]) {
126+
is List<*> -> optionsValue.filterIsInstance<String>()
127+
is String -> listOf(optionsValue)
128+
else -> emptyList()
129+
}
130+
val isVisible = attrMap["visible"] as? Boolean ?: true
131+
val isVariation = attrMap["variation"] as? Boolean ?: false
132+
return WooPosProductModelVersion2.WooPosProductAttribute(id, name, options, isVisible, isVariation)
133+
}
134+
135+
private fun parseCategories(categoriesJson: String): List<WooPosProductModelVersion2.WooPosProductCategory> {
136+
return try {
137+
if (categoriesJson.isBlank()) {
138+
emptyList()
139+
} else {
140+
val type = object : TypeToken<List<Map<String, Any?>>>() {}.type
141+
val categoriesList: List<Map<String, Any?>> = gson.fromJson(categoriesJson, type)
142+
categoriesList.mapNotNull { catMap ->
143+
return@mapNotNull parseCategory(catMap)
144+
}
145+
}
146+
} catch (e: JsonSyntaxException) {
147+
logger.w("Failed to parse categories JSON: $categoriesJson - ${e.message}")
148+
emptyList()
149+
}
150+
}
151+
152+
private fun parseCategory(catMap: Map<String, Any?>): WooPosProductModelVersion2.WooPosProductCategory? {
153+
val id = when (val idValue = catMap["id"]) {
154+
is Double -> idValue.toLong()
155+
is Int -> idValue.toLong()
156+
is Long -> idValue
157+
is String -> idValue.toLongOrNull()
158+
else -> null
159+
}
160+
val name = catMap["name"] as? String
161+
val slug = catMap["slug"] as? String ?: ""
162+
if (id == null || name.isNullOrBlank()) {
163+
logger.w("Failed to parse category JSON, id or name is null: $id, $name")
164+
return null
165+
}
166+
return WooPosProductModelVersion2.WooPosProductCategory(id, name, slug)
167+
}
168+
169+
private fun parseTags(tagsJson: String): List<WooPosProductModelVersion2.WooPosProductTag> {
170+
return try {
171+
if (tagsJson.isBlank()) {
172+
emptyList()
173+
} else {
174+
val type = object : TypeToken<List<Map<String, Any?>>>() {}.type
175+
val tagsList: List<Map<String, Any?>> = gson.fromJson(tagsJson, type)
176+
tagsList.mapNotNull { tagMap ->
177+
return@mapNotNull parseTag(tagMap)
178+
}
179+
}
180+
} catch (e: JsonSyntaxException) {
181+
logger.w("Failed to parse tags JSON: $tagsJson - ${e.message}")
182+
emptyList()
183+
}
184+
}
185+
186+
private fun parseTag(tagMap: Map<String, Any?>): WooPosProductModelVersion2.WooPosProductTag? {
187+
val id = when (val idValue = tagMap["id"]) {
188+
is Double -> idValue.toLong()
189+
is Int -> idValue.toLong()
190+
is Long -> idValue
191+
is String -> idValue.toLongOrNull()
192+
else -> null
193+
}
194+
val name = tagMap["name"] as? String
195+
val slug = tagMap["slug"] as? String ?: ""
196+
if (id == null || name.isNullOrBlank()) {
197+
logger.w("Failed to parse tag JSON, id or name is null: $id, $name")
198+
return null
199+
}
200+
return WooPosProductModelVersion2.WooPosProductTag(id, name, slug)
201+
}
202+
203+
fun mapPricing(
204+
price: BigDecimal?,
205+
regularPrice: BigDecimal?,
206+
salePrice: BigDecimal?,
207+
isOnSale: Boolean
208+
): WooPosProductModelVersion2.WooPosPricing {
209+
return when {
210+
isOnSale && salePrice != null && regularPrice != null -> {
211+
WooPosProductModelVersion2.WooPosPricing.SalePricing(regularPrice, salePrice)
212+
}
213+
214+
isOnSale && salePrice != null && price != null -> {
215+
WooPosProductModelVersion2.WooPosPricing.SalePricing(price, salePrice)
216+
}
217+
218+
regularPrice != null -> {
219+
WooPosProductModelVersion2.WooPosPricing.RegularPricing(regularPrice)
220+
}
221+
222+
price != null -> {
223+
WooPosProductModelVersion2.WooPosPricing.RegularPricing(price)
224+
}
225+
226+
else -> WooPosProductModelVersion2.WooPosPricing.NoPricing
227+
}
228+
}
229+
230+
fun mapProductType(type: String): WooPosProductModelVersion2.WooPosProductType {
231+
return when (type.lowercase()) {
232+
"simple" -> WooPosProductModelVersion2.WooPosProductType.SIMPLE
233+
"variable" -> WooPosProductModelVersion2.WooPosProductType.VARIABLE
234+
"grouped" -> WooPosProductModelVersion2.WooPosProductType.GROUPED
235+
"external" -> WooPosProductModelVersion2.WooPosProductType.EXTERNAL
236+
"variation" -> WooPosProductModelVersion2.WooPosProductType.VARIATION
237+
"subscription" -> WooPosProductModelVersion2.WooPosProductType.SUBSCRIPTION
238+
"variable-subscription" -> WooPosProductModelVersion2.WooPosProductType.VARIABLE_SUBSCRIPTION
239+
"bundle" -> WooPosProductModelVersion2.WooPosProductType.BUNDLE
240+
"composite" -> WooPosProductModelVersion2.WooPosProductType.COMPOSITE
241+
else -> WooPosProductModelVersion2.WooPosProductType.CUSTOM
242+
}
243+
}
244+
245+
fun mapProductStatus(status: String): WooPosProductModelVersion2.WooPosProductStatus {
246+
return when (status.lowercase()) {
247+
"publish" -> WooPosProductModelVersion2.WooPosProductStatus.PUBLISH
248+
"draft" -> WooPosProductModelVersion2.WooPosProductStatus.DRAFT
249+
"pending" -> WooPosProductModelVersion2.WooPosProductStatus.PENDING
250+
"private" -> WooPosProductModelVersion2.WooPosProductStatus.PRIVATE
251+
"trash" -> WooPosProductModelVersion2.WooPosProductStatus.TRASH
252+
else -> WooPosProductModelVersion2.WooPosProductStatus.UNKNOWN
253+
}
254+
}
255+
}

0 commit comments

Comments
 (0)