package sync import ( "fmt" "log/slog" "time" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/repository" ) // Service handles synchronization between MariaDB and local SQLite type Service struct { pricelistRepo *repository.PricelistRepository localDB *localdb.LocalDB } // NewService creates a new sync service func NewService(pricelistRepo *repository.PricelistRepository, localDB *localdb.LocalDB) *Service { return &Service{ pricelistRepo: pricelistRepo, localDB: localDB, } } // SyncStatus represents the current sync status type SyncStatus struct { LastSyncAt *time.Time `json:"last_sync_at"` ServerPricelists int `json:"server_pricelists"` LocalPricelists int `json:"local_pricelists"` NeedsSync bool `json:"needs_sync"` } // GetStatus returns the current sync status func (s *Service) GetStatus() (*SyncStatus, error) { lastSync := s.localDB.GetLastSyncTime() // Count server pricelists serverPricelists, _, err := s.pricelistRepo.List(0, 1) if err != nil { return nil, fmt.Errorf("counting server pricelists: %w", err) } // Count local pricelists localCount := s.localDB.CountLocalPricelists() needsSync, _ := s.NeedSync() return &SyncStatus{ LastSyncAt: lastSync, ServerPricelists: len(serverPricelists), LocalPricelists: int(localCount), NeedsSync: needsSync, }, nil } // NeedSync checks if synchronization is needed // Returns true if there are new pricelists on server or last sync was >1 hour ago func (s *Service) NeedSync() (bool, error) { lastSync := s.localDB.GetLastSyncTime() // If never synced, need sync if lastSync == nil { return true, nil } // If last sync was more than 1 hour ago, suggest sync if time.Since(*lastSync) > time.Hour { return true, nil } // Check if there are new pricelists on server latestServer, err := s.pricelistRepo.GetLatestActive() if err != nil { // If no pricelists on server, no need to sync return false, nil } latestLocal, err := s.localDB.GetLatestLocalPricelist() if err != nil { // No local pricelists, need to sync return true, nil } // If server has newer pricelist, need sync if latestServer.ID != latestLocal.ServerID { return true, nil } return false, nil } // SyncPricelists synchronizes all active pricelists from server to local SQLite func (s *Service) SyncPricelists() (int, error) { slog.Info("starting pricelist sync") // Get all active pricelists from server (up to 100) serverPricelists, _, err := s.pricelistRepo.List(0, 100) if err != nil { return 0, fmt.Errorf("getting server pricelists: %w", err) } synced := 0 for _, pl := range serverPricelists { // Check if pricelist already exists locally existing, _ := s.localDB.GetLocalPricelistByServerID(pl.ID) if existing != nil { // Already synced, skip continue } // Create local pricelist localPL := &localdb.LocalPricelist{ ServerID: pl.ID, Version: pl.Version, Name: pl.Notification, // Using notification as name CreatedAt: pl.CreatedAt, SyncedAt: time.Now(), IsUsed: false, } if err := s.localDB.SaveLocalPricelist(localPL); err != nil { slog.Warn("failed to save local pricelist", "version", pl.Version, "error", err) continue } synced++ slog.Debug("synced pricelist", "version", pl.Version, "server_id", pl.ID) } // Update last sync time s.localDB.SetLastSyncTime(time.Now()) slog.Info("pricelist sync completed", "synced", synced, "total", len(serverPricelists)) return synced, nil } // SyncPricelistItems synchronizes items for a specific pricelist func (s *Service) SyncPricelistItems(localPricelistID uint) (int, error) { // Get local pricelist localPL, err := s.localDB.GetLocalPricelistByID(localPricelistID) if err != nil { return 0, fmt.Errorf("getting local pricelist: %w", err) } // Check if items already exist existingCount := s.localDB.CountLocalPricelistItems(localPricelistID) if existingCount > 0 { slog.Debug("pricelist items already synced", "pricelist_id", localPricelistID, "count", existingCount) return int(existingCount), nil } // Get items from server serverItems, _, err := s.pricelistRepo.GetItems(localPL.ServerID, 0, 10000, "") if err != nil { return 0, fmt.Errorf("getting server pricelist items: %w", err) } // Convert and save locally localItems := make([]localdb.LocalPricelistItem, len(serverItems)) for i, item := range serverItems { localItems[i] = localdb.LocalPricelistItem{ PricelistID: localPricelistID, LotName: item.LotName, Price: item.Price, } } if err := s.localDB.SaveLocalPricelistItems(localItems); err != nil { return 0, fmt.Errorf("saving local pricelist items: %w", err) } slog.Info("synced pricelist items", "pricelist_id", localPricelistID, "items", len(localItems)) return len(localItems), nil } // SyncPricelistItemsByServerID syncs items for a pricelist by its server ID func (s *Service) SyncPricelistItemsByServerID(serverPricelistID uint) (int, error) { localPL, err := s.localDB.GetLocalPricelistByServerID(serverPricelistID) if err != nil { return 0, fmt.Errorf("local pricelist not found for server ID %d", serverPricelistID) } return s.SyncPricelistItems(localPL.ID) } // GetLocalPriceForLot returns the price for a lot from a local pricelist func (s *Service) GetLocalPriceForLot(localPricelistID uint, lotName string) (float64, error) { return s.localDB.GetLocalPriceForLot(localPricelistID, lotName) } // GetPricelistForOffline returns a pricelist suitable for offline use // If items are not synced, it will sync them first func (s *Service) GetPricelistForOffline(serverPricelistID uint) (*localdb.LocalPricelist, error) { // Ensure pricelist is synced localPL, err := s.localDB.GetLocalPricelistByServerID(serverPricelistID) if err != nil { // Try to sync pricelists first if _, err := s.SyncPricelists(); err != nil { return nil, fmt.Errorf("syncing pricelists: %w", err) } // Try again localPL, err = s.localDB.GetLocalPricelistByServerID(serverPricelistID) if err != nil { return nil, fmt.Errorf("pricelist not found on server: %w", err) } } // Ensure items are synced if _, err := s.SyncPricelistItems(localPL.ID); err != nil { return nil, fmt.Errorf("syncing pricelist items: %w", err) } return localPL, nil }