307 lines
9.5 KiB
Go
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
|
|
}
|