package warehouse import ( "math" "slices" "testing" "time" "git.mchus.pro/mchus/priceforge/internal/models" "github.com/glebarez/sqlite" "gorm.io/gorm" ) func TestComputePricelistItemsFromStockLog(t *testing.T) { db := openTestDB(t) if err := db.AutoMigrate(&models.Lot{}, &models.LotPartnumber{}, &models.StockLog{}); err != nil { t.Fatalf("automigrate: %v", err) } if err := db.Create(&models.Lot{LotName: "CPU_X"}).Error; err != nil { t.Fatalf("seed lot: %v", err) } if err := db.Create(&models.LotPartnumber{Partnumber: "PN-CPU-X", LotName: "CPU_X"}).Error; err != nil { t.Fatalf("seed mapping: %v", err) } qtySmall := 1.0 qtyBig := 9.0 now := time.Now() rows := []models.StockLog{ {Partnumber: "PN CPU X", Date: now, Price: 100, Qty: &qtySmall}, {Partnumber: "CPU_X-EXTRA", Date: now, Price: 200, Qty: &qtyBig}, } if err := db.Create(&rows).Error; err != nil { t.Fatalf("seed stock rows: %v", err) } items, err := ComputePricelistItemsFromStockLog(db) if err != nil { t.Fatalf("ComputePricelistItemsFromStockLog: %v", err) } if len(items) != 1 { t.Fatalf("expected 1 item, got %d", len(items)) } if items[0].LotName != "CPU_X" { t.Fatalf("expected lot CPU_X, got %s", items[0].LotName) } if math.Abs(items[0].Price-190) > 0.001 { t.Fatalf("expected weighted average 190, got %f", items[0].Price) } } func TestLoadLotMetricsLatestOnlyIncludesPartnumbers(t *testing.T) { db := openTestDB(t) if err := db.AutoMigrate(&models.Lot{}, &models.LotPartnumber{}, &models.StockLog{}); err != nil { t.Fatalf("automigrate: %v", err) } if err := db.Create(&models.Lot{LotName: "CPU_X"}).Error; err != nil { t.Fatalf("seed lot: %v", err) } if err := db.Create(&models.LotPartnumber{Partnumber: "PN-MAPPED", LotName: "CPU_X"}).Error; err != nil { t.Fatalf("seed mapping: %v", err) } oldDate := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) newDate := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC) oldQty := 10.0 newQty := 3.0 rows := []models.StockLog{ {Partnumber: "CPU_X-001", Date: oldDate, Price: 100, Qty: &oldQty}, {Partnumber: "CPU_X-001", Date: newDate, Price: 100, Qty: &newQty}, } if err := db.Create(&rows).Error; err != nil { t.Fatalf("seed stock rows: %v", err) } qtyByLot, pnsByLot, err := LoadLotMetrics(db, []string{"CPU_X"}, true) if err != nil { t.Fatalf("LoadLotMetrics: %v", err) } if got := qtyByLot["CPU_X"]; math.Abs(got-3.0) > 0.001 { t.Fatalf("expected latest qty 3, got %f", got) } pns := pnsByLot["CPU_X"] if !slices.Contains(pns, "PN-MAPPED") { t.Fatalf("expected mapped PN-MAPPED in partnumbers, got %v", pns) } if !slices.Contains(pns, "CPU_X-001") { t.Fatalf("expected stock CPU_X-001 in partnumbers, got %v", pns) } } func TestComputePricelistItemsFromStockLog_BundleAllocation(t *testing.T) { db := openTestDB(t) if err := db.AutoMigrate( &models.Lot{}, &models.LotPartnumber{}, &models.LotBundle{}, &models.LotBundleItem{}, &models.StockLog{}, ); err != nil { t.Fatalf("automigrate: %v", err) } if err := db.Exec(`CREATE TABLE qt_lot_metadata (lot_name TEXT PRIMARY KEY, current_price REAL NULL)`).Error; err != nil { t.Fatalf("create qt_lot_metadata: %v", err) } cat := "BUNDLE" if err := db.Create(&models.Lot{LotName: "BND_SERVER", LotCategory: &cat}).Error; err != nil { t.Fatalf("seed bundle lot: %v", err) } catComp := "COMP" if err := db.Create(&models.Lot{LotName: "CPU_X", LotCategory: &catComp}).Error; err != nil { t.Fatalf("seed cpu lot: %v", err) } if err := db.Create(&models.Lot{LotName: "RAM_X", LotCategory: &catComp}).Error; err != nil { t.Fatalf("seed ram lot: %v", err) } if err := db.Create(&models.LotPartnumber{Vendor: "VendorA", Partnumber: "SERVER-A", LotName: "BND_SERVER"}).Error; err != nil { t.Fatalf("seed mapping: %v", err) } if err := db.Create(&models.LotBundle{BundleLotName: "BND_SERVER", IsActive: true}).Error; err != nil { t.Fatalf("seed bundle: %v", err) } if err := db.Create(&[]models.LotBundleItem{ {BundleLotName: "BND_SERVER", LotName: "CPU_X", Qty: 2}, {BundleLotName: "BND_SERVER", LotName: "RAM_X", Qty: 1}, }).Error; err != nil { t.Fatalf("seed bundle items: %v", err) } cpuPrice := 100.0 ramPrice := 50.0 if err := db.Exec(`INSERT INTO qt_lot_metadata (lot_name, current_price) VALUES (?, ?)`, "CPU_X", cpuPrice).Error; err != nil { t.Fatalf("seed cpu meta: %v", err) } if err := db.Exec(`INSERT INTO qt_lot_metadata (lot_name, current_price) VALUES (?, ?)`, "RAM_X", ramPrice).Error; err != nil { t.Fatalf("seed ram meta: %v", err) } qty := 3.0 vendor := "VendorA" if err := db.Create(&models.StockLog{ Partnumber: "SERVER-A", Vendor: &vendor, Date: time.Now(), Price: 300, Qty: &qty, }).Error; err != nil { t.Fatalf("seed stock: %v", err) } items, err := ComputePricelistItemsFromStockLog(db) if err != nil { t.Fatalf("ComputePricelistItemsFromStockLog: %v", err) } if len(items) != 2 { t.Fatalf("expected 2 items, got %d", len(items)) } priceByLot := map[string]float64{} for _, item := range items { priceByLot[item.LotName] = item.Price } // Weights: CPU=2*100=200, RAM=1*50=50 => total=250; row price=300 // CPU=300*200/250=240, RAM=300*50/250=60 if math.Abs(priceByLot["CPU_X"]-240) > 0.001 { t.Fatalf("expected CPU_X 240, got %f", priceByLot["CPU_X"]) } if math.Abs(priceByLot["RAM_X"]-60) > 0.001 { t.Fatalf("expected RAM_X 60, got %f", priceByLot["RAM_X"]) } } func openTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } return db }