@@ -275,7 +275,7 @@ struct POSCatalogPersistenceServiceTests {
275275 }
276276 }
277277
278- @Test func persistIncrementalCatalogData_replaces_attributes_for_updated_product ( ) async throws {
278+ @Test func persistIncrementalCatalogData_upserts_attributes_for_updated_product ( ) async throws {
279279 // Given
280280 let attribute1 = Yosemite . ProductAttribute. fake ( ) . copy ( name: " Color " , options: [ " Indigo " , " Blue " ] )
281281 let attribute2 = Yosemite . ProductAttribute. fake ( ) . copy ( name: " Size " )
@@ -291,16 +291,17 @@ struct POSCatalogPersistenceServiceTests {
291291
292292 // Then
293293 try await grdbManager. databaseConnection. read { db in
294+ // Should have 4 attributes: original 2 + updated 2 (upsert adds new ones, keeps old ones)
294295 let attributeCount = try PersistedProductAttribute . fetchCount ( db)
295- #expect( attributeCount == 2 )
296+ #expect( attributeCount == 4 )
296297
297298 let attributes = try PersistedProductAttribute . fetchAll ( db) . sorted ( by: { $0. name < $1. name } )
298299 #expect( attributes [ 0 ] . name == " Color " )
299- #expect( attributes [ 0 ] . options == [ " Cardinal " , " Blue " ] )
300- #expect( attributes [ 0 ] . productID == 1 )
301- #expect( attributes [ 1 ] . name == " Material " )
302- #expect( attributes [ 1 ] . options == [ ] )
303- #expect( attributes [ 1 ] . productID == 1 )
300+ #expect( attributes [ 0 ] . options == [ " Indigo " , " Blue " ] ) // Original unchanged
301+ #expect( attributes [ 1 ] . name == " Color " )
302+ #expect( attributes [ 1 ] . options == [ " Cardinal " , " Blue " ] ) // Updated version
303+ #expect( attributes [ 2 ] . name == " Material " ) // New attribute
304+ #expect( attributes [ 3 ] . name == " Size " ) // Original unchanged
304305 }
305306 }
306307
@@ -320,24 +321,26 @@ struct POSCatalogPersistenceServiceTests {
320321
321322 // Then
322323 try await grdbManager. databaseConnection. read { db in
323- // Check join table has correct count
324+ // Check join table has correct count - should have 3 (original 2 + new 1, upsert keeps all)
324325 let joinCount = try PersistedProductImage . fetchCount ( db)
325- #expect( joinCount == 2 )
326+ #expect( joinCount == 3 )
326327
327328 // Check join table entries
328329 let joins = try PersistedProductImage . fetchAll ( db) . sorted ( by: { $0. imageID < $1. imageID } )
329- #expect( joins. count == 2 )
330+ #expect( joins. count == 3 )
330331 #expect( joins [ 0 ] . productID == 1 )
331332 #expect( joins [ 0 ] . imageID == 1 )
332333 #expect( joins [ 1 ] . productID == 1 )
333- #expect( joins [ 1 ] . imageID == 3 )
334+ #expect( joins [ 1 ] . imageID == 2 ) // Original image 2 join remains
335+ #expect( joins [ 2 ] . productID == 1 )
336+ #expect( joins [ 2 ] . imageID == 3 )
334337
335- // Check actual images
338+ // Check actual images - image 1 updated, image 2 unchanged, image 3 added
336339 let images = try PersistedImage . fetchAll ( db) . sorted ( by: { $0. id < $1. id } )
337- #expect( images. count == 3 ) // `image2` remains, but is unlinked.
338- #expect( images [ 0 ] . src == " https://example.com/image1-1.jpg " )
339- #expect( images [ 1 ] . src == " https://example.com/image2.jpg " )
340- #expect( images [ 2 ] . src == " https://example.com/image3.jpg " )
340+ #expect( images. count == 3 )
341+ #expect( images [ 0 ] . src == " https://example.com/image1-1.jpg " ) // Updated
342+ #expect( images [ 1 ] . src == " https://example.com/image2.jpg " ) // Unchanged
343+ #expect( images [ 2 ] . src == " https://example.com/image3.jpg " ) // New
341344 }
342345 }
343346
@@ -522,6 +525,100 @@ struct POSCatalogPersistenceServiceTests {
522525 }
523526 }
524527
528+ @Test func persistIncrementalCatalogData_deletes_variations_not_in_updated_product_variationIDs( ) async throws {
529+ // Given - a variable product with 3 variations
530+ let product = POSProduct . fake ( ) . copy (
531+ siteID: sampleSiteID,
532+ productID: 1 ,
533+ productTypeKey: " variable " ,
534+ variationIDs: [ 10 , 20 , 30 ]
535+ )
536+ let variation1 = POSProductVariation . fake ( ) . copy ( siteID: sampleSiteID, productID: 1 , productVariationID: 10 )
537+ let variation2 = POSProductVariation . fake ( ) . copy ( siteID: sampleSiteID, productID: 1 , productVariationID: 20 )
538+ let variation3 = POSProductVariation . fake ( ) . copy ( siteID: sampleSiteID, productID: 1 , productVariationID: 30 )
539+
540+ let catalog = POSCatalog ( products: [ product] , variations: [ variation1, variation2, variation3] , syncDate: . now)
541+ try await sut. replaceAllCatalogData ( catalog, siteID: sampleSiteID)
542+
543+ // Verify initial state
544+ try await db. read { db in
545+ let variationCount = try PersistedProductVariation . fetchCount ( db)
546+ #expect( variationCount == 3 )
547+ }
548+
549+ // When - incremental sync with updated product that only has 2 variations (removed variation 20)
550+ let updatedProduct = POSProduct . fake ( ) . copy (
551+ siteID: sampleSiteID,
552+ productID: 1 ,
553+ productTypeKey: " variable " ,
554+ variationIDs: [ 10 , 30 ] // variation 20 removed
555+ )
556+ let incrementalCatalog = POSCatalog ( products: [ updatedProduct] , variations: [ ] , syncDate: . now)
557+ try await sut. persistIncrementalCatalogData ( incrementalCatalog, siteID: sampleSiteID)
558+
559+ // Then - variation 20 should be deleted, variations 10 and 30 should remain
560+ try await db. read { db in
561+ let variationCount = try PersistedProductVariation . fetchCount ( db)
562+ #expect( variationCount == 2 )
563+
564+ let remainingVariations = try PersistedProductVariation . fetchAll ( db) . sorted ( by: { $0. id < $1. id } )
565+ #expect( remainingVariations. count == 2 )
566+ #expect( remainingVariations [ 0 ] . id == 10 )
567+ #expect( remainingVariations [ 1 ] . id == 30 )
568+ }
569+ }
570+
571+ @Test func persistIncrementalCatalogData_preserves_variations_not_mentioned_in_incremental_sync( ) async throws {
572+ // Given - two variable products with variations
573+ let product1 = POSProduct . fake ( ) . copy (
574+ siteID: sampleSiteID,
575+ productID: 1 ,
576+ productTypeKey: " variable " ,
577+ variationIDs: [ 10 , 20 ]
578+ )
579+ let product2 = POSProduct . fake ( ) . copy (
580+ siteID: sampleSiteID,
581+ productID: 2 ,
582+ productTypeKey: " variable " ,
583+ variationIDs: [ 30 , 40 ]
584+ )
585+ let variation10 = POSProductVariation . fake ( ) . copy ( siteID: sampleSiteID, productID: 1 , productVariationID: 10 )
586+ let variation20 = POSProductVariation . fake ( ) . copy ( siteID: sampleSiteID, productID: 1 , productVariationID: 20 )
587+ let variation30 = POSProductVariation . fake ( ) . copy ( siteID: sampleSiteID, productID: 2 , productVariationID: 30 )
588+ let variation40 = POSProductVariation . fake ( ) . copy ( siteID: sampleSiteID, productID: 2 , productVariationID: 40 )
589+
590+ let catalog = POSCatalog (
591+ products: [ product1, product2] ,
592+ variations: [ variation10, variation20, variation30, variation40] ,
593+ syncDate: . now
594+ )
595+ try await sut. replaceAllCatalogData ( catalog, siteID: sampleSiteID)
596+
597+ // When - incremental sync only updates product 1, product 2 not mentioned
598+ let updatedProduct1 = POSProduct . fake ( ) . copy (
599+ siteID: sampleSiteID,
600+ productID: 1 ,
601+ productTypeKey: " variable " ,
602+ variationIDs: [ 10 ] // removed variation 20
603+ )
604+ let incrementalCatalog = POSCatalog ( products: [ updatedProduct1] , variations: [ ] , syncDate: . now)
605+ try await sut. persistIncrementalCatalogData ( incrementalCatalog, siteID: sampleSiteID)
606+
607+ // Then - variation 20 deleted, but product 2's variations (30, 40) remain untouched
608+ try await db. read { db in
609+ let variationCount = try PersistedProductVariation . fetchCount ( db)
610+ #expect( variationCount == 3 )
611+
612+ let remainingVariations = try PersistedProductVariation . fetchAll ( db) . sorted ( by: { $0. id < $1. id } )
613+ #expect( remainingVariations [ 0 ] . id == 10 )
614+ #expect( remainingVariations [ 0 ] . productID == 1 )
615+ #expect( remainingVariations [ 1 ] . id == 30 )
616+ #expect( remainingVariations [ 1 ] . productID == 2 )
617+ #expect( remainingVariations [ 2 ] . id == 40 )
618+ #expect( remainingVariations [ 2 ] . productID == 2 )
619+ }
620+ }
621+
525622 @Test func persistIncrementalCatalogData_stores_incremental_sync_date( ) async throws {
526623 // Given - site with existing full sync date
527624 let fullSyncDate = Date ( ) . addingTimeInterval ( - 3600 ) // 1 hour ago
0 commit comments