154 lines
5.5 KiB
Go
154 lines
5.5 KiB
Go
package sync
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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"`
|
|
PartnumbersJSON string `gorm:"column:partnumbers_json"`
|
|
}
|
|
var serverBooks []serverBook
|
|
if err := mariaDB.Raw("SELECT id, version, created_at, is_active, partnumbers_json 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
|
|
partnumbers, errPartnumbers := decodeServerPartnumbers(sb.PartnumbersJSON)
|
|
if errPartnumbers != nil {
|
|
slog.Error("failed to decode server partnumbers_json", "server_id", sb.ID, "error", errPartnumbers)
|
|
continue
|
|
}
|
|
if err == nil {
|
|
existing.Version = sb.Version
|
|
existing.CreatedAt = sb.CreatedAt
|
|
existing.IsActive = sb.IsActive
|
|
existing.PartnumbersJSON = partnumbers
|
|
if err := localBookRepo.SaveBook(&existing); err != nil {
|
|
slog.Error("failed to update local partnumber book header", "server_id", sb.ID, "error", err)
|
|
continue
|
|
}
|
|
|
|
localItemCount := localBookRepo.CountBookItems(existing.ID)
|
|
if localItemCount > 0 && localBookRepo.HasAllBookItems(existing.ID) {
|
|
slog.Debug("partnumber book already synced, skipping", "server_id", sb.ID, "version", sb.Version, "items", localItemCount)
|
|
continue
|
|
}
|
|
slog.Info("partnumber book header exists but catalog items are missing, re-pulling items", "server_id", sb.ID, "version", sb.Version)
|
|
n, err := pullBookItems(mariaDB, localBookRepo, existing.PartnumbersJSON)
|
|
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,
|
|
PartnumbersJSON: partnumbers,
|
|
}
|
|
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, localBook.PartnumbersJSON)
|
|
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 catalog items for a partnumber list from MariaDB and saves them to SQLite.
|
|
// Returns the number of items saved.
|
|
func pullBookItems(mariaDB *gorm.DB, repo *repository.PartnumberBookRepository, partnumbers localdb.LocalStringList) (int, error) {
|
|
if len(partnumbers) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
type serverItem struct {
|
|
Partnumber string `gorm:"column:partnumber"`
|
|
LotsJSON string `gorm:"column:lots_json"`
|
|
Description string `gorm:"column:description"`
|
|
}
|
|
var serverItems []serverItem
|
|
err := mariaDB.Raw("SELECT partnumber, lots_json, description FROM qt_partnumber_book_items WHERE partnumber IN ?", []string(partnumbers)).Scan(&serverItems).Error
|
|
if err != nil {
|
|
return 0, fmt.Errorf("querying items from server: %w", err)
|
|
}
|
|
slog.Info("partnumber book items fetched from server", "count", len(serverItems), "requested_partnumbers", len(partnumbers))
|
|
|
|
if len(serverItems) == 0 {
|
|
slog.Warn("server returned 0 partnumber book items")
|
|
return 0, nil
|
|
}
|
|
|
|
localItems := make([]localdb.LocalPartnumberBookItem, 0, len(serverItems))
|
|
for _, si := range serverItems {
|
|
var lots localdb.LocalPartnumberBookLots
|
|
if err := json.Unmarshal([]byte(si.LotsJSON), &lots); err != nil {
|
|
return 0, fmt.Errorf("decode lots_json for %s: %w", si.Partnumber, err)
|
|
}
|
|
localItems = append(localItems, localdb.LocalPartnumberBookItem{
|
|
Partnumber: si.Partnumber,
|
|
LotsJSON: lots,
|
|
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
|
|
}
|
|
|
|
func decodeServerPartnumbers(raw string) (localdb.LocalStringList, error) {
|
|
if raw == "" {
|
|
return localdb.LocalStringList{}, nil
|
|
}
|
|
var items []string
|
|
if err := json.Unmarshal([]byte(raw), &items); err != nil {
|
|
return nil, err
|
|
}
|
|
return localdb.LocalStringList(items), nil
|
|
}
|