package services import ( "fmt" "testing" "git.mchus.pro/mchus/priceforge/internal/models" "github.com/glebarez/sqlite" "gorm.io/gorm" ) func TestUpsertMappingWithOriginalVendor_DeletesEmptyOriginalVendorRow(t *testing.T) { db, err := gorm.Open(sqlite.Open(testSQLiteDSN(t)), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } stmts := []string{ `CREATE TABLE lot ( lot_name TEXT PRIMARY KEY, lot_description TEXT, lot_category TEXT )`, `CREATE TABLE qt_vendor_partnumber_seen ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_type TEXT NOT NULL, vendor TEXT NOT NULL DEFAULT '', partnumber TEXT NOT NULL UNIQUE, description TEXT NULL, last_seen_at DATETIME NOT NULL, is_ignored INTEGER NOT NULL DEFAULT 0, is_pattern INTEGER NOT NULL DEFAULT 0, ignored_at DATETIME NULL, ignored_by TEXT NULL, created_at DATETIME NULL, updated_at DATETIME NULL )`, `CREATE TABLE qt_partnumber_book_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, partnumber TEXT NOT NULL UNIQUE, lots_json TEXT NOT NULL, description TEXT NULL )`, } for _, stmt := range stmts { if err := db.Exec(stmt).Error; err != nil { t.Fatalf("exec schema: %v", err) } } if err := db.Create(&models.Lot{LotName: "MB_INTEL_6.6700/6500(GNR-SP,SRF-SP)_2S_32xDDR5"}).Error; err != nil { t.Fatalf("insert lot: %v", err) } if err := db.Create(&models.PartnumberBookItem{ Partnumber: "7DG9-CTO1WW", LotsJSON: `[{"lot_name":"MB_INTEL_6.6700/6500(GNR-SP,SRF-SP)_2S_32xDDR5","qty":1}]`, }).Error; err != nil { t.Fatalf("insert original mapping: %v", err) } svc := NewVendorMappingService(db) if err := svc.UpsertMappingWithOriginalVendor( "", "Lenovo", "7DG9-CTO1WW", "MB_INTEL_6.6700/6500(GNR-SP,SRF-SP)_2S_32xDDR5", "", nil, ); err != nil { t.Fatalf("upsert mapping: %v", err) } var rows []models.PartnumberBookItem if err := db.Order("partnumber").Find(&rows).Error; err != nil { t.Fatalf("list mappings: %v", err) } if len(rows) != 1 { t.Fatalf("got %d mapping rows, want 1", len(rows)) } if rows[0].Partnumber != "7DG9-CTO1WW" { t.Fatalf("got partnumber %q, want %q", rows[0].Partnumber, "7DG9-CTO1WW") } var seen models.VendorPartnumberSeen if err := db.Where("partnumber = ?", "7DG9-CTO1WW").First(&seen).Error; err != nil { t.Fatalf("load seen row: %v", err) } if seen.Vendor != "Lenovo" { t.Fatalf("got vendor %q, want %q", seen.Vendor, "Lenovo") } } func TestUpsertMappingWithOriginalVendor_PreservesDescriptionFromEmptyOriginalVendorRow(t *testing.T) { db, err := gorm.Open(sqlite.Open(testSQLiteDSN(t)), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } stmts := []string{ `CREATE TABLE lot ( lot_name TEXT PRIMARY KEY, lot_description TEXT, lot_category TEXT )`, `CREATE TABLE qt_vendor_partnumber_seen ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_type TEXT NOT NULL, vendor TEXT NOT NULL DEFAULT '', partnumber TEXT NOT NULL UNIQUE, description TEXT NULL, last_seen_at DATETIME NOT NULL, is_ignored INTEGER NOT NULL DEFAULT 0, is_pattern INTEGER NOT NULL DEFAULT 0, ignored_at DATETIME NULL, ignored_by TEXT NULL, created_at DATETIME NULL, updated_at DATETIME NULL )`, `CREATE TABLE qt_partnumber_book_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, partnumber TEXT NOT NULL UNIQUE, lots_json TEXT NOT NULL, description TEXT NULL )`, } for _, stmt := range stmts { if err := db.Exec(stmt).Error; err != nil { t.Fatalf("exec schema: %v", err) } } const lotName = "MB_INTEL_6.6700/6500(GNR-SP,SRF-SP)_2S_32xDDR5" const wantDescription = "Existing description" if err := db.Create(&models.Lot{LotName: lotName}).Error; err != nil { t.Fatalf("insert lot: %v", err) } if err := db.Create(&models.PartnumberBookItem{ Partnumber: "7DG9-CTO1WW", LotsJSON: fmt.Sprintf(`[{"lot_name":%q,"qty":1}]`, lotName), Description: stringPtr(wantDescription), }).Error; err != nil { t.Fatalf("insert original mapping: %v", err) } svc := NewVendorMappingService(db) if err := svc.UpsertMappingWithOriginalVendor("", "Lenovo", "7DG9-CTO1WW", lotName, "", nil); err != nil { t.Fatalf("upsert mapping: %v", err) } var row models.PartnumberBookItem if err := db.Where("partnumber = ?", "7DG9-CTO1WW").First(&row).Error; err != nil { t.Fatalf("load new mapping: %v", err) } if row.Description == nil || *row.Description != wantDescription { got := "" if row.Description != nil { got = *row.Description } t.Fatalf("got description %q, want %q", got, wantDescription) } } func stringPtr(v string) *string { return &v } func testSQLiteDSN(t *testing.T) string { return fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) } func TestPurgeSeenLotNames_RemovesPollutedSeenRowsButKeepsMappedPartnumbers(t *testing.T) { db, err := gorm.Open(sqlite.Open(testSQLiteDSN(t)), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } stmts := []string{ `CREATE TABLE lot ( lot_name TEXT PRIMARY KEY, lot_description TEXT, lot_category TEXT )`, `CREATE TABLE qt_partnumber_book_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, partnumber TEXT NOT NULL UNIQUE, lots_json TEXT NOT NULL, description TEXT NULL )`, `CREATE TABLE qt_vendor_partnumber_seen ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_type TEXT NOT NULL, vendor TEXT NOT NULL DEFAULT '', partnumber TEXT NOT NULL UNIQUE, description TEXT NULL, last_seen_at DATETIME NOT NULL, is_ignored INTEGER NOT NULL DEFAULT 0, is_pattern INTEGER NOT NULL DEFAULT 0, ignored_at DATETIME NULL, ignored_by TEXT NULL, created_at DATETIME NULL, updated_at DATETIME NULL )`, } for _, stmt := range stmts { if err := db.Exec(stmt).Error; err != nil { t.Fatalf("exec schema: %v", err) } } now := "2026-03-07 00:00:00" if err := db.Exec(`INSERT INTO lot (lot_name) VALUES (?), (?)`, "LOT_NAME_1", "LOT_NAME_2").Error; err != nil { t.Fatalf("insert lots: %v", err) } if err := db.Exec(`INSERT INTO qt_partnumber_book_items (partnumber, lots_json) VALUES (?, ?)`, "LOT_NAME_2", `[{"lot_name":"LOT_NAME_2","qty":1}]`).Error; err != nil { t.Fatalf("insert mapped pn: %v", err) } if err := db.Exec(`INSERT INTO qt_vendor_partnumber_seen (source_type, vendor, partnumber, last_seen_at) VALUES (?, ?, ?, ?), (?, ?, ?, ?)`, "stock", "", "LOT_NAME_1", now, "stock", "", "LOT_NAME_2", now, ).Error; err != nil { t.Fatalf("insert seen rows: %v", err) } if err := purgeSeenLotNames(db); err != nil { t.Fatalf("purge seen lot names: %v", err) } var rows []models.VendorPartnumberSeen if err := db.Order("partnumber").Find(&rows).Error; err != nil { t.Fatalf("load seen rows: %v", err) } if len(rows) != 1 { t.Fatalf("got %d seen rows, want 1", len(rows)) } if rows[0].Partnumber != "LOT_NAME_2" { t.Fatalf("got partnumber %q, want %q", rows[0].Partnumber, "LOT_NAME_2") } }