package repository import ( "fmt" "strings" "time" "git.mchus.pro/mchus/quoteforge/internal/models" "gorm.io/gorm" ) type PricelistRepository struct { db *gorm.DB } func NewPricelistRepository(db *gorm.DB) *PricelistRepository { return &PricelistRepository{db: db} } // List returns pricelists with pagination func (r *PricelistRepository) List(offset, limit int) ([]models.PricelistSummary, int64, error) { var total int64 if err := r.db.Model(&models.Pricelist{}).Count(&total).Error; err != nil { return nil, 0, fmt.Errorf("counting pricelists: %w", err) } var pricelists []models.Pricelist if err := r.db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&pricelists).Error; err != nil { return nil, 0, fmt.Errorf("listing pricelists: %w", err) } // Get item counts for each pricelist summaries := make([]models.PricelistSummary, len(pricelists)) for i, pl := range pricelists { var itemCount int64 r.db.Model(&models.PricelistItem{}).Where("pricelist_id = ?", pl.ID).Count(&itemCount) summaries[i] = models.PricelistSummary{ ID: pl.ID, Version: pl.Version, Notification: pl.Notification, CreatedAt: pl.CreatedAt, CreatedBy: pl.CreatedBy, IsActive: pl.IsActive, UsageCount: pl.UsageCount, ExpiresAt: pl.ExpiresAt, ItemCount: itemCount, } } return summaries, total, nil } // GetByID returns a pricelist by ID func (r *PricelistRepository) GetByID(id uint) (*models.Pricelist, error) { var pricelist models.Pricelist if err := r.db.First(&pricelist, id).Error; err != nil { return nil, fmt.Errorf("getting pricelist: %w", err) } // Get item count var itemCount int64 r.db.Model(&models.PricelistItem{}).Where("pricelist_id = ?", id).Count(&itemCount) pricelist.ItemCount = int(itemCount) return &pricelist, nil } // GetByVersion returns a pricelist by version string func (r *PricelistRepository) GetByVersion(version string) (*models.Pricelist, error) { var pricelist models.Pricelist if err := r.db.Where("version = ?", version).First(&pricelist).Error; err != nil { return nil, fmt.Errorf("getting pricelist by version: %w", err) } return &pricelist, nil } // GetLatestActive returns the most recent active pricelist func (r *PricelistRepository) GetLatestActive() (*models.Pricelist, error) { var pricelist models.Pricelist if err := r.db.Where("is_active = ?", true).Order("created_at DESC").First(&pricelist).Error; err != nil { return nil, fmt.Errorf("getting latest pricelist: %w", err) } return &pricelist, nil } // Create creates a new pricelist func (r *PricelistRepository) Create(pricelist *models.Pricelist) error { if err := r.db.Create(pricelist).Error; err != nil { return fmt.Errorf("creating pricelist: %w", err) } return nil } // Update updates a pricelist func (r *PricelistRepository) Update(pricelist *models.Pricelist) error { if err := r.db.Save(pricelist).Error; err != nil { return fmt.Errorf("updating pricelist: %w", err) } return nil } // Delete deletes a pricelist if usage_count is 0 func (r *PricelistRepository) Delete(id uint) error { pricelist, err := r.GetByID(id) if err != nil { return err } if pricelist.UsageCount > 0 { return fmt.Errorf("cannot delete pricelist with usage_count > 0 (current: %d)", pricelist.UsageCount) } // Delete items first if err := r.db.Where("pricelist_id = ?", id).Delete(&models.PricelistItem{}).Error; err != nil { return fmt.Errorf("deleting pricelist items: %w", err) } // Delete pricelist if err := r.db.Delete(&models.Pricelist{}, id).Error; err != nil { return fmt.Errorf("deleting pricelist: %w", err) } return nil } // CreateItems batch inserts pricelist items func (r *PricelistRepository) CreateItems(items []models.PricelistItem) error { if len(items) == 0 { return nil } // Use batch insert for better performance batchSize := 500 for i := 0; i < len(items); i += batchSize { end := i + batchSize if end > len(items) { end = len(items) } if err := r.db.CreateInBatches(items[i:end], batchSize).Error; err != nil { return fmt.Errorf("batch inserting pricelist items: %w", err) } } return nil } // GetItems returns pricelist items with pagination func (r *PricelistRepository) GetItems(pricelistID uint, offset, limit int, search string) ([]models.PricelistItem, int64, error) { var total int64 query := r.db.Model(&models.PricelistItem{}).Where("pricelist_id = ?", pricelistID) if search != "" { query = query.Where("lot_name LIKE ?", "%"+search+"%") } if err := query.Count(&total).Error; err != nil { return nil, 0, fmt.Errorf("counting pricelist items: %w", err) } var items []models.PricelistItem if err := query.Order("lot_name").Offset(offset).Limit(limit).Find(&items).Error; err != nil { return nil, 0, fmt.Errorf("listing pricelist items: %w", err) } // Enrich with lot descriptions for i := range items { var lot models.Lot if err := r.db.Where("lot_name = ?", items[i].LotName).First(&lot).Error; err == nil { items[i].LotDescription = lot.LotDescription } // Parse category from lot_name (e.g., "CPU_AMD_9654" -> "CPU") parts := strings.SplitN(items[i].LotName, "_", 2) if len(parts) >= 1 { items[i].Category = parts[0] } } return items, total, nil } // GenerateVersion generates a new version string in format YYYY-MM-DD-NNN func (r *PricelistRepository) GenerateVersion() (string, error) { today := time.Now().Format("2006-01-02") var count int64 if err := r.db.Model(&models.Pricelist{}). Where("version LIKE ?", today+"%"). Count(&count).Error; err != nil { return "", fmt.Errorf("counting today's pricelists: %w", err) } return fmt.Sprintf("%s-%03d", today, count+1), nil } // CanWrite checks if the current database user has INSERT permission on qt_pricelists func (r *PricelistRepository) CanWrite() bool { canWrite, _ := r.CanWriteDebug() return canWrite } // CanWriteDebug checks write permission and returns debug info // Uses raw SQL with explicit columns to avoid schema mismatch issues func (r *PricelistRepository) CanWriteDebug() (bool, string) { // Check if table exists first var count int64 if err := r.db.Table("qt_pricelists").Count(&count).Error; err != nil { return false, fmt.Sprintf("table check failed: %v", err) } // Use raw SQL with only essential columns that always exist // This avoids GORM model validation and schema mismatch issues tx := r.db.Begin() if tx.Error != nil { return false, fmt.Sprintf("begin tx failed: %v", tx.Error) } defer tx.Rollback() // Always rollback - this is just a permission test testVersion := fmt.Sprintf("test-%06d", time.Now().Unix()%1000000) // Raw SQL insert with only core columns err := tx.Exec(` INSERT INTO qt_pricelists (version, created_by, is_active) VALUES (?, 'system', 1) `, testVersion).Error if err != nil { // Check if it's a permission error vs other errors errStr := err.Error() if strings.Contains(errStr, "INSERT command denied") || strings.Contains(errStr, "Access denied") { return false, "no write permission" } return false, fmt.Sprintf("insert failed: %v", err) } return true, "ok" } // IncrementUsageCount increments the usage count for a pricelist func (r *PricelistRepository) IncrementUsageCount(id uint) error { return r.db.Model(&models.Pricelist{}).Where("id = ?", id). UpdateColumn("usage_count", gorm.Expr("usage_count + 1")).Error } // DecrementUsageCount decrements the usage count for a pricelist func (r *PricelistRepository) DecrementUsageCount(id uint) error { return r.db.Model(&models.Pricelist{}).Where("id = ?", id). UpdateColumn("usage_count", gorm.Expr("GREATEST(usage_count - 1, 0)")).Error } // GetExpiredUnused returns pricelists that are expired and unused func (r *PricelistRepository) GetExpiredUnused() ([]models.Pricelist, error) { var pricelists []models.Pricelist if err := r.db.Where("expires_at < ? AND usage_count = 0", time.Now()). Find(&pricelists).Error; err != nil { return nil, fmt.Errorf("getting expired pricelists: %w", err) } return pricelists, nil }