Files
QuoteForge/internal/repository/pricelist.go
2026-02-06 10:14:24 +03:00

307 lines
9.5 KiB
Go

package repository
import (
"errors"
"fmt"
"strconv"
"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)
}
return r.toSummaries(pricelists), total, nil
}
// ListActive returns active pricelists with pagination.
func (r *PricelistRepository) ListActive(offset, limit int) ([]models.PricelistSummary, int64, error) {
var total int64
if err := r.db.Model(&models.Pricelist{}).Where("is_active = ?", true).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("counting active pricelists: %w", err)
}
var pricelists []models.Pricelist
if err := r.db.Where("is_active = ?", true).Order("created_at DESC").Offset(offset).Limit(limit).Find(&pricelists).Error; err != nil {
return nil, 0, fmt.Errorf("listing active pricelists: %w", err)
}
return r.toSummaries(pricelists), total, nil
}
// CountActive returns the number of active pricelists.
func (r *PricelistRepository) CountActive() (int64, error) {
var total int64
if err := r.db.Model(&models.Pricelist{}).Where("is_active = ?", true).Count(&total).Error; err != nil {
return 0, fmt.Errorf("counting active pricelists: %w", err)
}
return total, nil
}
func (r *PricelistRepository) toSummaries(pricelists []models.Pricelist) []models.PricelistSummary {
// 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
}
// 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 last models.Pricelist
err := r.db.Model(&models.Pricelist{}).
Select("version").
Where("version LIKE ?", today+"-%").
Order("version DESC").
Limit(1).
Take(&last).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Sprintf("%s-001", today), nil
}
return "", fmt.Errorf("loading latest today's pricelist version: %w", err)
}
parts := strings.Split(last.Version, "-")
if len(parts) < 4 {
return "", fmt.Errorf("invalid pricelist version format: %s", last.Version)
}
n, err := strconv.Atoi(parts[len(parts)-1])
if err != nil {
return "", fmt.Errorf("parsing pricelist sequence %q: %w", parts[len(parts)-1], err)
}
return fmt.Sprintf("%s-%03d", today, n+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
}