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 @@ -55,7 +55,8 @@ struct V001InitialSchema {

private static func createProductAttributeTable(_ db: Database) throws {
try db.create(table: "productAttribute") { productAttributeTable in
// This table holds local product attributes only. Global attributes belong to a site.
// This table holds both local and global product attributes.
// Local attributes have remoteAttributeID = 0, global attributes have remoteAttributeID > 0
productAttributeTable.autoIncrementedPrimaryKey("id").notNull()
productAttributeTable.column("siteID", .integer).notNull()
productAttributeTable.column("productID", .integer).notNull()
Expand All @@ -64,6 +65,7 @@ struct V001InitialSchema {
columns: ["siteID", "id"],
onDelete: .cascade)

productAttributeTable.column("remoteAttributeID", .integer).notNull()
productAttributeTable.column("name", .text).notNull()
productAttributeTable.column("position", .integer).notNull()
productAttributeTable.column("visible", .boolean).notNull()
Expand Down Expand Up @@ -119,7 +121,8 @@ struct V001InitialSchema {

private static func createProductVariationAttributeTable(_ db: Database) throws {
try db.create(table: "productVariationAttribute") { productVariationAttributeTable in
// This table holds local variation attributes only. Global attributes belong to a site.
// This table holds both local and global variation attributes.
// Local attributes have remoteAttributeID = 0, global attributes have remoteAttributeID > 0
productVariationAttributeTable.autoIncrementedPrimaryKey("id").notNull()
productVariationAttributeTable.column("siteID", .integer).notNull()
productVariationAttributeTable.column("productVariationID", .integer).notNull()
Expand All @@ -128,6 +131,7 @@ struct V001InitialSchema {
columns: ["siteID", "id"],
onDelete: .cascade)

productVariationAttributeTable.column("remoteAttributeID", .integer).notNull()
productVariationAttributeTable.column("name", .text).notNull()
productVariationAttributeTable.column("option", .text).notNull()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public struct PersistedProductAttribute: Codable {
public private(set) var id: Int64?
public let siteID: Int64
public let productID: Int64
public let remoteAttributeID: Int64
public let name: String
public let position: Int64
public let visible: Bool
Expand All @@ -15,6 +16,7 @@ public struct PersistedProductAttribute: Codable {
public init(id: Int64? = nil,
siteID: Int64,
productID: Int64,
remoteAttributeID: Int64,
name: String,
position: Int64,
visible: Bool,
Expand All @@ -23,6 +25,7 @@ public struct PersistedProductAttribute: Codable {
self.id = id
self.siteID = siteID
self.productID = productID
self.remoteAttributeID = remoteAttributeID
self.name = name
self.position = position
self.visible = visible
Expand All @@ -41,6 +44,7 @@ extension PersistedProductAttribute: FetchableRecord, MutablePersistableRecord {
public static let id = Column(CodingKeys.id)
public static let siteID = Column(CodingKeys.siteID)
public static let productID = Column(CodingKeys.productID)
public static let remoteAttributeID = Column(CodingKeys.remoteAttributeID)
public static let name = Column(CodingKeys.name)
public static let position = Column(CodingKeys.position)
public static let visible = Column(CodingKeys.visible)
Expand All @@ -62,6 +66,7 @@ extension PersistedProductAttribute {
case id
case siteID
case productID
case remoteAttributeID
case name
case position
case visible
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ public struct PersistedProductVariationAttribute: Codable {
public private(set) var id: Int64?
public let siteID: Int64
public let productVariationID: Int64
public let remoteAttributeID: Int64
public let name: String
public let option: String

public init(id: Int64? = nil,
siteID: Int64,
productVariationID: Int64,
remoteAttributeID: Int64,
name: String,
option: String) {
self.id = id
self.siteID = siteID
self.productVariationID = productVariationID
self.remoteAttributeID = remoteAttributeID
self.name = name
self.option = option
}
Expand All @@ -33,6 +36,7 @@ extension PersistedProductVariationAttribute: FetchableRecord, MutablePersistabl
public static let id = Column(CodingKeys.id)
public static let siteID = Column(CodingKeys.siteID)
public static let productVariationID = Column(CodingKeys.productVariationID)
public static let remoteAttributeID = Column(CodingKeys.remoteAttributeID)
public static let name = Column(CodingKeys.name)
public static let option = Column(CodingKeys.option)
}
Expand All @@ -51,6 +55,7 @@ extension PersistedProductVariationAttribute {
case id
case siteID
case productVariationID
case remoteAttributeID
case name
case option
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ extension PersistedProductAttribute {
self.init(
siteID: siteID,
productID: productID,
remoteAttributeID: productAttribute.attributeID,
name: productAttribute.name,
position: Int64(productAttribute.position),
visible: productAttribute.visible,
Expand All @@ -100,7 +101,7 @@ extension PersistedProductAttribute {
func toProductAttribute(siteID: Int64) -> ProductAttribute {
return ProductAttribute(
siteID: siteID,
attributeID: id ?? 0,
attributeID: remoteAttributeID,
name: name,
position: Int(position),
visible: visible,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,15 @@ extension PersistedProductVariationAttribute {
self.init(
siteID: siteID,
productVariationID: productVariationID,
remoteAttributeID: productVariationAttribute.id,
name: productVariationAttribute.name,
option: productVariationAttribute.option
)
}

func toProductVariationAttribute() -> ProductVariationAttribute {
return ProductVariationAttribute(
id: id ?? 0,
id: remoteAttributeID,
name: name,
option: option
)
Expand Down
8 changes: 8 additions & 0 deletions Modules/Tests/StorageTests/GRDB/GRDBManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ struct GRDBManagerTests {
let attribute = TestProductAttribute(
siteID: 1,
productID: 100,
remoteAttributeID: 0,
name: "Color",
position: 0,
visible: true,
Expand Down Expand Up @@ -247,6 +248,7 @@ struct GRDBManagerTests {
let variationAttribute = TestProductVariationAttribute(
siteID: 1,
productVariationID: 200,
remoteAttributeID: 0,
name: "Color",
option: "Red"
)
Expand Down Expand Up @@ -300,6 +302,7 @@ struct GRDBManagerTests {
let productAttribute = TestProductAttribute(
siteID: testSiteId,
productID: 100,
remoteAttributeID: 50,
name: "Color",
position: 0,
visible: true,
Expand All @@ -311,6 +314,7 @@ struct GRDBManagerTests {
let variationAttribute = TestProductVariationAttribute(
siteID: testSiteId,
productVariationID: 200,
remoteAttributeID: 60,
name: "Size",
option: "Large"
)
Expand Down Expand Up @@ -563,6 +567,7 @@ struct GRDBManagerTests {
let productAttribute = TestProductAttribute(
siteID: siteID,
productID: product.id,
remoteAttributeID: 50,
name: "Color \(i)",
position: i,
visible: true,
Expand All @@ -575,6 +580,7 @@ struct GRDBManagerTests {
let variationAttribute = TestProductVariationAttribute(
siteID: siteID,
productVariationID: variation.id,
remoteAttributeID: 60,
name: "Size \(i)",
option: "Large"
)
Expand Down Expand Up @@ -744,6 +750,7 @@ extension TestProductVariation: FetchableRecord, PersistableRecord {
struct TestProductAttribute: Codable {
let siteID: Int64
let productID: Int64
let remoteAttributeID: Int64
let name: String
let position: Int
let visible: Bool
Expand All @@ -758,6 +765,7 @@ extension TestProductAttribute: FetchableRecord, PersistableRecord {
struct TestProductVariationAttribute: Codable {
let siteID: Int64
let productVariationID: Int64
let remoteAttributeID: Int64
let name: String
let option: String
}
Expand Down
131 changes: 129 additions & 2 deletions Modules/Tests/YosemiteTests/Storage/PersistedProductTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,22 @@ struct PersistedProductTests {
]

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

// When
Expand Down Expand Up @@ -178,6 +192,7 @@ struct PersistedProductTests {
var attribute1 = PersistedProductAttribute(
siteID: 1,
productID: 100,
remoteAttributeID: 501,
name: "Color",
position: 0,
visible: true,
Expand All @@ -187,6 +202,7 @@ struct PersistedProductTests {
var attribute2 = PersistedProductAttribute(
siteID: 1,
productID: 100,
remoteAttributeID: 502,
name: "Size",
position: 1,
visible: false,
Expand Down Expand Up @@ -312,6 +328,117 @@ struct PersistedProductTests {
#expect(back.options == attribute.options)
}

@Test("PersistedProductAttribute with remoteAttributeID of 0 represents non-global attribute")
func product_attribute_with_zero_remote_id_is_non_global() throws {
// Given a non-global attribute (not shared across products)
let siteID: Int64 = 10
let productID: Int64 = 100
let attribute = ProductAttribute(siteID: siteID,
attributeID: 0,
name: "Custom Color",
position: 0,
visible: true,
variation: true,
options: ["Custom Red", "Custom Blue"])

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

// Then the remoteAttributeID should be 0 for non-global attributes
#expect(persisted.remoteAttributeID == 0)
#expect(persisted.name == "Custom Color")
#expect(persisted.options == ["Custom Red", "Custom Blue"])

// When converting back to ProductAttribute
let back = persisted.toProductAttribute(siteID: siteID)

// Then the attributeID should remain 0
#expect(back.attributeID == 0)
#expect(back.name == "Custom Color")
#expect(back.options == ["Custom Red", "Custom Blue"])
}

@Test("Non-global product attribute persists and retrieves correctly with remoteAttributeID 0")
func non_global_product_attribute_persists_correctly() throws {
// Given
let grdbManager = try GRDBManager()
let db = grdbManager.databaseConnection

try db.write { db in
let site = PersistedSite(id: 5)
try site.insert(db)

let product = PersistedProduct(
id: 500,
siteID: 5,
name: "Custom Product",
productTypeKey: "variable",
fullDescription: nil,
shortDescription: nil,
sku: nil,
globalUniqueID: nil,
price: "25.00",
downloadable: false,
parentID: 0,
manageStock: false,
stockQuantity: nil,
stockStatusKey: "instock"
)
try product.insert(db)

// Insert a non-global attribute with remoteAttributeID = 0
var nonGlobalAttribute = PersistedProductAttribute(
siteID: 5,
productID: 500,
remoteAttributeID: 0,
name: "Custom Texture",
position: 0,
visible: true,
variation: true,
options: ["Smooth", "Rough", "Textured"]
)
try nonGlobalAttribute.insert(db)

// Also insert a global attribute for comparison
var globalAttribute = PersistedProductAttribute(
siteID: 5,
productID: 500,
remoteAttributeID: 123,
name: "Standard Size",
position: 1,
visible: true,
variation: false,
options: ["Small", "Medium", "Large"]
)
try globalAttribute.insert(db)
}

// When
let fetchedProduct = try db.read { db in
try PersistedProduct.filter(PersistedProduct.Columns.id == 500).fetchOne(db)
}

let product = try #require(fetchedProduct)
let posProduct = try product.toPOSProduct(db: db)

// Then both attributes should be present
#expect(posProduct.attributes.count == 2)

// Verify non-global attribute
let nonGlobalAttr = posProduct.attributes.first { $0.name == "Custom Texture" }
#expect(nonGlobalAttr != nil)
#expect(nonGlobalAttr?.attributeID == 0)
#expect(nonGlobalAttr?.options == ["Smooth", "Rough", "Textured"])
#expect(nonGlobalAttr?.variation == true)

// Verify global attribute
let globalAttr = posProduct.attributes.first { $0.name == "Standard Size" }
#expect(globalAttr != nil)
#expect(globalAttr?.attributeID == 123)
#expect(globalAttr?.options == ["Small", "Medium", "Large"])
#expect(globalAttr?.variation == false)
}

@Test("PersistedProductImage init(from:) and toProductImage round-trip")
func product_image_round_trip() throws {
// Given
Expand Down
Loading