Update pricelist repository, service, and tests
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -210,14 +212,31 @@ func (r *PricelistRepository) GetItems(pricelistID uint, offset, limit int, sear
|
|||||||
func (r *PricelistRepository) GenerateVersion() (string, error) {
|
func (r *PricelistRepository) GenerateVersion() (string, error) {
|
||||||
today := time.Now().Format("2006-01-02")
|
today := time.Now().Format("2006-01-02")
|
||||||
|
|
||||||
var count int64
|
var last models.Pricelist
|
||||||
if err := r.db.Model(&models.Pricelist{}).
|
err := r.db.Model(&models.Pricelist{}).
|
||||||
Where("version LIKE ?", today+"%").
|
Select("version").
|
||||||
Count(&count).Error; err != nil {
|
Where("version LIKE ?", today+"-%").
|
||||||
return "", fmt.Errorf("counting today's pricelists: %w", err)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s-%03d", today, count+1), nil
|
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
|
// CanWrite checks if the current database user has INSERT permission on qt_pricelists
|
||||||
|
|||||||
64
internal/repository/pricelist_test.go
Normal file
64
internal/repository/pricelist_test.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateVersion_FirstOfDay(t *testing.T) {
|
||||||
|
repo := newTestPricelistRepository(t)
|
||||||
|
|
||||||
|
version, err := repo.GenerateVersion()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateVersion returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
want := fmt.Sprintf("%s-001", today)
|
||||||
|
if version != want {
|
||||||
|
t.Fatalf("expected %s, got %s", want, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateVersion_UsesMaxSuffixNotCount(t *testing.T) {
|
||||||
|
repo := newTestPricelistRepository(t)
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
|
||||||
|
seed := []models.Pricelist{
|
||||||
|
{Version: fmt.Sprintf("%s-001", today), CreatedBy: "test", IsActive: true},
|
||||||
|
{Version: fmt.Sprintf("%s-003", today), CreatedBy: "test", IsActive: true},
|
||||||
|
}
|
||||||
|
for _, pl := range seed {
|
||||||
|
if err := repo.Create(&pl); err != nil {
|
||||||
|
t.Fatalf("seed insert failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := repo.GenerateVersion()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateVersion returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := fmt.Sprintf("%s-004", today)
|
||||||
|
if version != want {
|
||||||
|
t.Fatalf("expected %s, got %s", want, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestPricelistRepository(t *testing.T) *PricelistRepository {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&models.Pricelist{}); err != nil {
|
||||||
|
t.Fatalf("migrate: %v", err)
|
||||||
|
}
|
||||||
|
return NewPricelistRepository(db)
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package pricelist
|
package pricelist
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||||
@@ -30,22 +32,35 @@ func (s *Service) CreateFromCurrentPrices(createdBy string) (*models.Pricelist,
|
|||||||
return nil, fmt.Errorf("offline mode: cannot create pricelists")
|
return nil, fmt.Errorf("offline mode: cannot create pricelists")
|
||||||
}
|
}
|
||||||
|
|
||||||
version, err := s.repo.GenerateVersion()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("generating version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expiresAt := time.Now().AddDate(1, 0, 0) // +1 year
|
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{
|
pricelist = &models.Pricelist{
|
||||||
Version: version,
|
Version: version,
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
ExpiresAt: &expiresAt,
|
ExpiresAt: &expiresAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.repo.Create(pricelist); err != nil {
|
if err := s.repo.Create(pricelist); err != nil {
|
||||||
return nil, fmt.Errorf("creating pricelist: %w", err)
|
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
|
// Get all components with prices from qt_lot_metadata
|
||||||
@@ -90,6 +105,14 @@ func (s *Service) CreateFromCurrentPrices(createdBy string) (*models.Pricelist,
|
|||||||
return pricelist, nil
|
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
|
// List returns pricelists with pagination
|
||||||
func (s *Service) List(page, perPage int) ([]models.PricelistSummary, int64, error) {
|
func (s *Service) List(page, perPage int) ([]models.PricelistSummary, int64, error) {
|
||||||
// If no database connection (offline mode), return empty list
|
// If no database connection (offline mode), return empty list
|
||||||
|
|||||||
Reference in New Issue
Block a user