package sync import ( "fmt" "log/slog" "time" "git.mchus.pro/mchus/priceforge/internal/localdb" "git.mchus.pro/mchus/priceforge/internal/models" "git.mchus.pro/mchus/priceforge/internal/repository" ) // SyncPricelists synchronizes all active pricelists from server to local SQLite func (s *Service) SyncPricelists() (int, error) { slog.Info("starting pricelist sync") if _, err := s.EnsureReadinessForSync(); err != nil { return 0, err } // Get database connection mariaDB, err := s.getDB() if err != nil { return 0, fmt.Errorf("database not available: %w", err) } // Create repository pricelistRepo := repository.NewPricelistRepository(mariaDB) // Get active pricelists from server (up to 100) serverPricelists, _, err := pricelistRepo.ListActive(0, 100) if err != nil { return 0, fmt.Errorf("getting active server pricelists: %w", err) } synced := 0 var latestEstimateLocalID uint var latestEstimateCreatedAt time.Time for _, pl := range serverPricelists { // Check if pricelist already exists locally existing, _ := s.localDB.GetLocalPricelistByServerID(pl.ID) if existing != nil { // Track latest estimate pricelist by created_at for component refresh. if pl.Source == string(models.PricelistSourceEstimate) && (latestEstimateCreatedAt.IsZero() || pl.CreatedAt.After(latestEstimateCreatedAt)) { latestEstimateCreatedAt = pl.CreatedAt latestEstimateLocalID = existing.ID } continue } // Create local pricelist localPL := &localdb.LocalPricelist{ ServerID: pl.ID, Source: pl.Source, 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 } // Sync items for the newly created pricelist itemCount, err := s.SyncPricelistItems(localPL.ID) if err != nil { slog.Warn("failed to sync pricelist items", "version", pl.Version, "error", err) // Continue even if items sync fails - we have the pricelist metadata } else { slog.Debug("synced pricelist with items", "version", pl.Version, "items", itemCount) } if pl.Source == string(models.PricelistSourceEstimate) && (latestEstimateCreatedAt.IsZero() || pl.CreatedAt.After(latestEstimateCreatedAt)) { latestEstimateCreatedAt = pl.CreatedAt latestEstimateLocalID = localPL.ID } synced++ } // Update component prices from latest estimate pricelist only. if latestEstimateLocalID > 0 { updated, err := s.localDB.UpdateComponentPricesFromPricelist(latestEstimateLocalID) if err != nil { slog.Warn("failed to update component prices from pricelist", "error", err) } else { slog.Info("updated component prices from latest pricelist", "updated", updated) } } // Update last sync time s.localDB.SetLastSyncTime(time.Now()) s.RecordSyncHeartbeat() 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 database connection mariaDB, err := s.getDB() if err != nil { return 0, fmt.Errorf("database not available: %w", err) } // Create repository pricelistRepo := repository.NewPricelistRepository(mariaDB) // Get items from server serverItems, _, err := 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 { partnumbers := make(localdb.LocalStringList, 0, len(item.Partnumbers)) partnumbers = append(partnumbers, item.Partnumbers...) localItems[i] = localdb.LocalPricelistItem{ PricelistID: localPricelistID, LotName: item.LotName, Price: item.Price, AvailableQty: item.AvailableQty, Partnumbers: partnumbers, } } 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 } // SyncPricelistsIfNeeded checks for new pricelists and syncs if needed // This should be called before creating a new configuration when online func (s *Service) SyncPricelistsIfNeeded() error { needSync, err := s.NeedSync() if err != nil { slog.Warn("failed to check if sync needed", "error", err) return nil // Don't fail on check error } if !needSync { slog.Debug("pricelists are up to date, no sync needed") return nil } slog.Info("new pricelists detected, syncing...") _, err = s.SyncPricelists() if err != nil { return fmt.Errorf("syncing pricelists: %w", err) } return nil }