package sync import ( "fmt" "log/slog" "time" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/repository" "gorm.io/gorm" ) // PullPartnumberBooks synchronizes partnumber book snapshots from MariaDB to local SQLite. // Append-only for headers; re-pulls items if a book header exists but has 0 items. func (s *Service) PullPartnumberBooks() (int, error) { slog.Info("starting partnumber book pull") mariaDB, err := s.getDB() if err != nil { return 0, fmt.Errorf("database not available: %w", err) } localBookRepo := repository.NewPartnumberBookRepository(s.localDB.DB()) type serverBook struct { ID int `gorm:"column:id"` Version string `gorm:"column:version"` CreatedAt time.Time `gorm:"column:created_at"` IsActive bool `gorm:"column:is_active"` } var serverBooks []serverBook if err := mariaDB.Raw("SELECT id, version, created_at, is_active FROM qt_partnumber_books ORDER BY created_at DESC, id DESC").Scan(&serverBooks).Error; err != nil { return 0, fmt.Errorf("querying server partnumber books: %w", err) } slog.Info("partnumber books found on server", "count", len(serverBooks)) pulled := 0 for _, sb := range serverBooks { var existing localdb.LocalPartnumberBook err := s.localDB.DB().Where("server_id = ?", sb.ID).First(&existing).Error if err == nil { // Header exists — check whether items were saved localItemCount := localBookRepo.CountBookItems(existing.ID) if localItemCount > 0 { slog.Debug("partnumber book already synced, skipping", "server_id", sb.ID, "version", sb.Version, "items", localItemCount) continue } // Items missing — re-pull them slog.Info("partnumber book header exists but has no items, re-pulling items", "server_id", sb.ID, "version", sb.Version) n, err := pullBookItems(mariaDB, localBookRepo, sb.ID, existing.ID) if err != nil { slog.Error("failed to re-pull items for existing book", "server_id", sb.ID, "error", err) } else { slog.Info("re-pulled items for existing book", "server_id", sb.ID, "version", sb.Version, "items_saved", n) pulled++ } continue } slog.Info("pulling new partnumber book", "server_id", sb.ID, "version", sb.Version, "is_active", sb.IsActive) localBook := &localdb.LocalPartnumberBook{ ServerID: sb.ID, Version: sb.Version, CreatedAt: sb.CreatedAt, IsActive: sb.IsActive, } if err := localBookRepo.SaveBook(localBook); err != nil { slog.Error("failed to save local partnumber book", "server_id", sb.ID, "error", err) continue } n, err := pullBookItems(mariaDB, localBookRepo, sb.ID, localBook.ID) if err != nil { slog.Error("failed to pull items for new book", "server_id", sb.ID, "error", err) continue } slog.Info("partnumber book saved locally", "server_id", sb.ID, "version", sb.Version, "items_saved", n) pulled++ } slog.Info("partnumber book pull completed", "new_books_pulled", pulled, "total_on_server", len(serverBooks)) return pulled, nil } // pullBookItems fetches items for a single book from MariaDB and saves them to SQLite. // Returns the number of items saved. func pullBookItems(mariaDB *gorm.DB, repo *repository.PartnumberBookRepository, serverBookID int, localBookID uint) (int, error) { type serverItem struct { Partnumber string `gorm:"column:partnumber"` LotName string `gorm:"column:lot_name"` IsPrimaryPN bool `gorm:"column:is_primary_pn"` Description string `gorm:"column:description"` } // description column may not exist yet on older server schemas — query without it first, // then retry with it to populate descriptions if available. var serverItems []serverItem err := mariaDB.Raw("SELECT partnumber, lot_name, is_primary_pn, description FROM qt_partnumber_book_items WHERE book_id = ?", serverBookID).Scan(&serverItems).Error if err != nil { slog.Warn("description column not available on server, retrying without it", "server_book_id", serverBookID, "error", err) if err2 := mariaDB.Raw("SELECT partnumber, lot_name, is_primary_pn FROM qt_partnumber_book_items WHERE book_id = ?", serverBookID).Scan(&serverItems).Error; err2 != nil { return 0, fmt.Errorf("querying items from server: %w", err2) } } slog.Info("partnumber book items fetched from server", "server_book_id", serverBookID, "count", len(serverItems)) if len(serverItems) == 0 { slog.Warn("server returned 0 items for book — check qt_partnumber_book_items on server", "server_book_id", serverBookID) return 0, nil } localItems := make([]localdb.LocalPartnumberBookItem, 0, len(serverItems)) for _, si := range serverItems { localItems = append(localItems, localdb.LocalPartnumberBookItem{ BookID: localBookID, Partnumber: si.Partnumber, LotName: si.LotName, IsPrimaryPN: si.IsPrimaryPN, Description: si.Description, }) } if err := repo.SaveBookItems(localItems); err != nil { return 0, fmt.Errorf("saving items to local db: %w", err) } return len(localItems), nil }