Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f4cf155
Add mapping for persisted models
joshheald Aug 29, 2025
b37d574
Add attribute and image associations
joshheald Aug 29, 2025
ff03881
Test persisted to networking model conversion
joshheald Aug 29, 2025
f416265
Save product/variation entities with relationships
joshheald Aug 29, 2025
627787d
Fix whitespace
joshheald Aug 29, 2025
e831e89
Test saving POS models and regroup mapping tests
joshheald Aug 29, 2025
e9fc1ce
Tests for saving POS models to GRDB
joshheald Aug 29, 2025
9f46de1
Fix test expectation
joshheald Aug 29, 2025
f822a09
Match attribute handling for product and variation
joshheald Sep 1, 2025
218d704
Test updates from code revier
joshheald Sep 1, 2025
cdc2e9d
Test fix
joshheald Sep 1, 2025
e3d7b85
Use a single transaction for reading relationships
joshheald Sep 1, 2025
435502f
Merge branch 'woomob-1210-basic-mapping-from-POS-entities-to-persiste…
joshheald Sep 1, 2025
b9c3b19
Merge branch 'woomob-1210-relationship-based-mapping-and-saving-for-P…
joshheald Sep 1, 2025
5c8d942
Remove test prefixes
joshheald Sep 1, 2025
a9048a2
Demonstrate joined records in variation tests
joshheald Sep 1, 2025
991e405
Given/When/Then for all tests
joshheald Sep 1, 2025
c523e48
Fix lint
joshheald Sep 1, 2025
2477ef4
Ignore periphery
joshheald Sep 1, 2025
604a3df
More periphery
joshheald Sep 1, 2025
d0c1b2d
[Woo POS][Local Catalog] Tests for saving pos models (#16064)
joshheald Sep 1, 2025
2159f0d
[Woo POS][Local catalog] Add relationship mapping and saving for POS …
joshheald Sep 1, 2025
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
@@ -0,0 +1,97 @@
import Foundation
import Storage

// MARK: - PersistedProduct Conversions
extension PersistedProduct {
public init(from posProduct: POSProduct) {
self.init(
id: posProduct.productID,
siteID: posProduct.siteID,
name: posProduct.name,
productTypeKey: posProduct.productTypeKey,
fullDescription: posProduct.fullDescription,
shortDescription: posProduct.shortDescription,
sku: posProduct.sku,
globalUniqueID: posProduct.globalUniqueID,
price: posProduct.price,
downloadable: posProduct.downloadable,
parentID: posProduct.parentID,
manageStock: posProduct.manageStock,
stockQuantity: posProduct.stockQuantity,
stockStatusKey: posProduct.stockStatusKey
)
}

public func toPOSProduct(images: [ProductImage] = [], attributes: [ProductAttribute] = []) -> POSProduct {
return POSProduct(
siteID: siteID,
productID: id,
name: name,
productTypeKey: productTypeKey,
fullDescription: fullDescription,
shortDescription: shortDescription,
sku: sku,
globalUniqueID: globalUniqueID,
price: price,
downloadable: downloadable,
parentID: parentID,
images: images,
attributes: attributes,
manageStock: manageStock,
stockQuantity: stockQuantity,
stockStatusKey: stockStatusKey
)
}
}

// MARK: - PersistedProductAttribute Conversions
extension PersistedProductAttribute {
public init(from productAttribute: ProductAttribute, productID: Int64) {
self.init(
productID: productID,
name: productAttribute.name,
position: Int64(productAttribute.position),
visible: productAttribute.visible,
variation: productAttribute.variation,
options: productAttribute.options
)
}

public func toProductAttribute(siteID: Int64) -> ProductAttribute {
return ProductAttribute(
siteID: siteID,
attributeID: 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

if we decide to set the remote ID in a separate column (#16047 (comment)), this can be set. For now, do we want to set this with id ?? 0 like in the variation attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in f822a09

The risk (which was already worse than it is now) is that if we call toProductAttribute on a global attribute which hasn't been saved, we'll get 0 instead of nil, which suggests a local attribute. But then only local attributes are properly handled so far anyway.

name: name,
position: Int(position),
visible: visible,
variation: variation,
options: options
)
}
}

// MARK: - PersistedProductImage Conversions
extension PersistedProductImage {
public init(from productImage: ProductImage, productID: Int64) {
self.init(
id: productImage.imageID,
productID: productID,
dateCreated: productImage.dateCreated,
dateModified: productImage.dateModified,
src: productImage.src,
name: productImage.name,
alt: productImage.alt
)
}

public func toProductImage() -> ProductImage {
return ProductImage(
imageID: id,
dateCreated: dateCreated,
dateModified: dateModified,
src: src,
name: name,
alt: alt
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Foundation
import Storage

// MARK: - PersistedProductVariation Conversions
extension PersistedProductVariation {
public init(from posProductVariation: POSProductVariation) {
self.init(
id: posProductVariation.productVariationID,
siteID: posProductVariation.siteID,
productID: posProductVariation.productID,
sku: posProductVariation.sku,
globalUniqueID: posProductVariation.globalUniqueID,
price: posProductVariation.price,
downloadable: posProductVariation.downloadable,
fullDescription: posProductVariation.fullDescription,
manageStock: posProductVariation.manageStock,
stockQuantity: posProductVariation.stockQuantity,
stockStatusKey: posProductVariation.stockStatusKey
)
}

public func toPOSProductVariation(attributes: [ProductVariationAttribute] = [], image: ProductImage? = nil) -> POSProductVariation {
return POSProductVariation(
siteID: siteID,
productID: productID,
productVariationID: id,
attributes: attributes,
image: image,
fullDescription: fullDescription,
sku: sku,
globalUniqueID: globalUniqueID,
price: price,
downloadable: downloadable,
manageStock: manageStock,
stockQuantity: stockQuantity,
stockStatusKey: stockStatusKey
)
}
}

// MARK: - PersistedProductVariationAttribute Conversions
extension PersistedProductVariationAttribute {
public init(from productVariationAttribute: ProductVariationAttribute, productVariationID: Int64) {
self.init(
productVariationID: productVariationID,
name: productVariationAttribute.name,
option: productVariationAttribute.option
)
}

public func toProductVariationAttribute() -> ProductVariationAttribute {
return ProductVariationAttribute(
id: id ?? 0,
name: name,
option: option
)
}
}

// MARK: - PersistedProductVariationImage Conversions
extension PersistedProductVariationImage {
public init(from productImage: ProductImage, productVariationID: Int64) {
self.init(
id: productImage.imageID,
productVariationID: productVariationID,
dateCreated: productImage.dateCreated,
dateModified: productImage.dateModified,
src: productImage.src,
name: productImage.name,
alt: productImage.alt
)
}

public func toProductImage() -> ProductImage {
return ProductImage(
imageID: id,
dateCreated: dateCreated,
dateModified: dateModified,
src: src,
name: name,
alt: alt
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import Foundation
import Testing
@testable import Yosemite

struct PersistedProductConversionsTests {

@Test("PersistedProduct init(from:) maps all POSProduct fields")
func test_product_init_from_posProduct_maps_all_fields() throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

super nit: with Swift Testing, we probably don't need the test_ prefix in the function name as each test case is already marked with @Test.

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'll update them. TBH I think we could also leave behind the snake case, if we use descriptive names as arguments to @Test – they display much nicer anyway, at least within Xcode. I'm not sure whether CI pays any attention to them though.

// Given
let siteID: Int64 = 1
let productID: Int64 = 10
let images: [ProductImage] = [
ProductImage(imageID: 100,
dateCreated: Date(timeIntervalSince1970: 1),
dateModified: nil,
src: "https://example.com/1.png",
name: "img1",
alt: "alt1"),
ProductImage(imageID: 101,
dateCreated: Date(timeIntervalSince1970: 2),
dateModified: Date(timeIntervalSince1970: 3),
src: "https://example.com/2.png",
name: "img2",
alt: nil)
]
let attributes: [ProductAttribute] = [
ProductAttribute(siteID: siteID, attributeID: 0, name: "Color", position: 0, visible: true, variation: true, options: ["Red", "Blue"]),
ProductAttribute(siteID: siteID, attributeID: 0, name: "Size", position: 1, visible: false, variation: false, options: ["S", "M"])
]
Copy link
Contributor

Choose a reason for hiding this comment

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

super nit: since these aren't being asserted on in this test case, maybe can pass empty arrays for simplicity.

let pos = POSProduct(
siteID: siteID,
productID: productID,
name: "Test Product",
productTypeKey: "simple",
fullDescription: "Full",
shortDescription: "Short",
sku: "SKU-123",
globalUniqueID: "GID-1",
price: "9.99",
downloadable: false,
parentID: 0,
images: images,
attributes: attributes,
manageStock: true,
stockQuantity: 5,
stockStatusKey: "instock"
)

// When
let persisted = PersistedProduct(from: pos)

// Then
#expect(persisted.id == productID)
#expect(persisted.siteID == siteID)
#expect(persisted.name == pos.name)
#expect(persisted.productTypeKey == pos.productTypeKey)
#expect(persisted.fullDescription == pos.fullDescription)
#expect(persisted.shortDescription == pos.shortDescription)
#expect(persisted.sku == pos.sku)
#expect(persisted.globalUniqueID == pos.globalUniqueID)
#expect(persisted.price == pos.price)
#expect(persisted.downloadable == pos.downloadable)
#expect(persisted.parentID == pos.parentID)
#expect(persisted.manageStock == pos.manageStock)
#expect(persisted.stockQuantity == pos.stockQuantity)
#expect(persisted.stockStatusKey == pos.stockStatusKey)
}

@Test("PersistedProduct toPOSProduct maps back with images and attributes")
func test_product_toPOSProduct_maps_back_including_images_and_attributes() throws {
// Given
let siteID: Int64 = 2
let productID: Int64 = 20
let persisted = PersistedProduct(
id: productID,
siteID: siteID,
name: "Prod",
productTypeKey: "variable",
fullDescription: "FullD",
shortDescription: "ShortD",
sku: nil,
globalUniqueID: nil,
price: "12.34",
downloadable: true,
parentID: 0,
manageStock: false,
stockQuantity: nil,
stockStatusKey: "outofstock"
)

let productImages = [
PersistedProductImage(id: 200,
productID: productID,
dateCreated: Date(timeIntervalSince1970: 10),
dateModified: nil,
src: "https://example.com/p1.png",
name: "p1",
alt: "a1"),
PersistedProductImage(id: 201,
productID: productID,
dateCreated: Date(timeIntervalSince1970: 11),
dateModified: Date(timeIntervalSince1970: 12),
src: "https://example.com/p2.png",
name: nil,
alt: nil)
]

let persistedAttributes = [
PersistedProductAttribute(productID: productID, name: "Material", position: 0, visible: true, variation: false, options: ["Cotton"]),
PersistedProductAttribute(productID: productID, name: "Fit", position: 1, visible: true, variation: true, options: ["Slim", "Regular"])
]

// When
let pos = persisted.toPOSProduct(
images: productImages.map { $0.toProductImage() },
attributes: persistedAttributes.map { $0.toProductAttribute(siteID: siteID) }
)

// Then
#expect(pos.siteID == siteID)
#expect(pos.productID == productID)
#expect(pos.name == persisted.name)
#expect(pos.productTypeKey == persisted.productTypeKey)
#expect(pos.fullDescription == persisted.fullDescription)
#expect(pos.shortDescription == persisted.shortDescription)
#expect(pos.sku == persisted.sku)
#expect(pos.globalUniqueID == persisted.globalUniqueID)
#expect(pos.price == persisted.price)
#expect(pos.downloadable == persisted.downloadable)
#expect(pos.parentID == persisted.parentID)
#expect(pos.manageStock == persisted.manageStock)
#expect(pos.stockQuantity == persisted.stockQuantity)
#expect(pos.stockStatusKey == persisted.stockStatusKey)
#expect(pos.images.count == 2)
#expect(pos.attributes.count == 2)
#expect(pos.attributesForVariations.count == 1)
}

@Test("PersistedProductAttribute init(from:) and toProductAttribute round-trip")
func test_product_attribute_round_trip() throws {
// Given
let siteID: Int64 = 3
let productID: Int64 = 30
let attribute = ProductAttribute(siteID: siteID,
attributeID: 0,
name: "Flavor",
position: 2,
visible: true,
variation: true,
options: ["Vanilla", "Chocolate"])

// When
let persisted = PersistedProductAttribute(from: attribute, productID: productID)
let back = persisted.toProductAttribute(siteID: siteID)

// Then
#expect(persisted.productID == productID)
#expect(persisted.name == attribute.name)
#expect(persisted.position == Int64(attribute.position))
#expect(persisted.visible == attribute.visible)
#expect(persisted.variation == attribute.variation)
#expect(persisted.options == attribute.options)

#expect(back.siteID == siteID)
#expect(back.name == attribute.name)
#expect(back.position == attribute.position)
#expect(back.visible == attribute.visible)
#expect(back.variation == attribute.variation)
#expect(back.options == attribute.options)
}

@Test("PersistedProductImage init(from:) and toProductImage round-trip")
func test_product_image_round_trip() throws {
// Given
let productID: Int64 = 40
let image = ProductImage(imageID: 400,
dateCreated: Date(timeIntervalSince1970: 100),
dateModified: Date(timeIntervalSince1970: 200),
src: "https://example.com/x.png",
name: "x",
alt: "y")

// When
let persisted = PersistedProductImage(from: image, productID: productID)
let back = persisted.toProductImage()

// Then
#expect(persisted.id == image.imageID)
#expect(persisted.productID == productID)
#expect(persisted.dateCreated == image.dateCreated)
#expect(persisted.dateModified == image.dateModified)
#expect(persisted.src == image.src)
#expect(persisted.name == image.name)
#expect(persisted.alt == image.alt)

#expect(back.imageID == image.imageID)
#expect(back.dateCreated == image.dateCreated)
#expect(back.dateModified == image.dateModified)
#expect(back.src == image.src)
#expect(back.name == image.name)
#expect(back.alt == image.alt)
}
}
Loading