package pricelist import ( "errors" "fmt" "log/slog" "strings" "time" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/mchus/quoteforge/internal/repository" "gorm.io/gorm" ) type Service struct { repo *repository.PricelistRepository componentRepo *repository.ComponentRepository db *gorm.DB } func NewService(db *gorm.DB, repo *repository.PricelistRepository, componentRepo *repository.ComponentRepository) *Service { return &Service{ repo: repo, componentRepo: componentRepo, db: db, } } // CreateFromCurrentPrices creates a new pricelist by taking a snapshot of current prices func (s *Service) CreateFromCurrentPrices(createdBy string) (*models.Pricelist, error) { if s.repo == nil || s.db == nil { return nil, fmt.Errorf("offline mode: cannot create pricelists") } expiresAt := time.Now().AddDate(1, 0, 0) // +1 year const maxCreateAttempts = 5 var pricelist *models.Pricelist for attempt := 1; attempt <= maxCreateAttempts; attempt++ { version, err := s.repo.GenerateVersion() if err != nil { return nil, fmt.Errorf("generating version: %w", err) } pricelist = &models.Pricelist{ Version: version, CreatedBy: createdBy, IsActive: true, ExpiresAt: &expiresAt, } if err := s.repo.Create(pricelist); err != nil { if isVersionConflictError(err) && attempt < maxCreateAttempts { slog.Warn("pricelist version conflict, retrying", "attempt", attempt, "version", version, "error", err, ) time.Sleep(time.Duration(attempt*25) * time.Millisecond) continue } return nil, fmt.Errorf("creating pricelist: %w", err) } break } // Get all components with prices from qt_lot_metadata var metadata []models.LotMetadata if err := s.db.Where("current_price IS NOT NULL AND current_price > 0").Find(&metadata).Error; err != nil { return nil, fmt.Errorf("getting lot metadata: %w", err) } // Create pricelist items with all price settings items := make([]models.PricelistItem, 0, len(metadata)) for _, m := range metadata { if m.CurrentPrice == nil || *m.CurrentPrice <= 0 { continue } items = append(items, models.PricelistItem{ PricelistID: pricelist.ID, LotName: m.LotName, Price: *m.CurrentPrice, PriceMethod: string(m.PriceMethod), PricePeriodDays: m.PricePeriodDays, PriceCoefficient: m.PriceCoefficient, ManualPrice: m.ManualPrice, MetaPrices: m.MetaPrices, }) } if err := s.repo.CreateItems(items); err != nil { // Clean up the pricelist if items creation fails s.repo.Delete(pricelist.ID) return nil, fmt.Errorf("creating pricelist items: %w", err) } pricelist.ItemCount = len(items) slog.Info("pricelist created", "id", pricelist.ID, "version", pricelist.Version, "items", len(items), "created_by", createdBy, ) return pricelist, nil } func isVersionConflictError(err error) bool { if errors.Is(err, gorm.ErrDuplicatedKey) { return true } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "duplicate entry") && strings.Contains(msg, "idx_qt_pricelists_version") } // List returns pricelists with pagination func (s *Service) List(page, perPage int) ([]models.PricelistSummary, int64, error) { // If no database connection (offline mode), return empty list if s.repo == nil { return []models.PricelistSummary{}, 0, nil } if page < 1 { page = 1 } if perPage < 1 { perPage = 20 } offset := (page - 1) * perPage return s.repo.List(offset, perPage) } // GetByID returns a pricelist by ID func (s *Service) GetByID(id uint) (*models.Pricelist, error) { if s.repo == nil { return nil, fmt.Errorf("offline mode: pricelist service not available") } return s.repo.GetByID(id) } // GetItems returns pricelist items with pagination func (s *Service) GetItems(pricelistID uint, page, perPage int, search string) ([]models.PricelistItem, int64, error) { if s.repo == nil { return []models.PricelistItem{}, 0, nil } if page < 1 { page = 1 } if perPage < 1 { perPage = 50 } offset := (page - 1) * perPage return s.repo.GetItems(pricelistID, offset, perPage, search) } // Delete deletes a pricelist by ID func (s *Service) Delete(id uint) error { if s.repo == nil { return fmt.Errorf("offline mode: cannot delete pricelists") } return s.repo.Delete(id) } // CanWrite returns true if the user can create pricelists func (s *Service) CanWrite() bool { if s.repo == nil { return false } return s.repo.CanWrite() } // CanWriteDebug returns write permission status with debug info func (s *Service) CanWriteDebug() (bool, string) { if s.repo == nil { return false, "offline mode" } return s.repo.CanWriteDebug() } // GetLatestActive returns the most recent active pricelist func (s *Service) GetLatestActive() (*models.Pricelist, error) { if s.repo == nil { return nil, fmt.Errorf("offline mode: pricelist service not available") } return s.repo.GetLatestActive() } // CleanupExpired deletes expired and unused pricelists func (s *Service) CleanupExpired() (int, error) { if s.repo == nil { return 0, fmt.Errorf("offline mode: cleanup not available") } expired, err := s.repo.GetExpiredUnused() if err != nil { return 0, err } deleted := 0 for _, pl := range expired { if err := s.repo.Delete(pl.ID); err != nil { slog.Warn("failed to delete expired pricelist", "id", pl.ID, "error", err) continue } deleted++ } slog.Info("cleaned up expired pricelists", "deleted", deleted) return deleted, nil }