fix(qfs): project ui, config naming, sync timestamps - v1.5.4

This commit is contained in:
Mikhail Chusavitin
2026-03-16 08:32:15 +03:00
parent c599897142
commit 35c5600b36
16 changed files with 815 additions and 93 deletions

View File

@@ -20,6 +20,7 @@ var (
ErrProjectForbidden = errors.New("access to project forbidden")
ErrProjectCodeExists = errors.New("project code and variant already exist")
ErrCannotDeleteMainVariant = errors.New("cannot delete main variant")
ErrReservedMainVariant = errors.New("variant name 'main' is reserved")
)
type ProjectService struct {
@@ -63,6 +64,9 @@ func (s *ProjectService) Create(ownerUsername string, req *CreateProjectRequest)
return nil, fmt.Errorf("project code is required")
}
variant := strings.TrimSpace(req.Variant)
if err := validateProjectVariantName(variant); err != nil {
return nil, err
}
if err := s.ensureUniqueProjectCodeVariant("", code, variant); err != nil {
return nil, err
}
@@ -105,6 +109,9 @@ func (s *ProjectService) Update(projectUUID, ownerUsername string, req *UpdatePr
}
if req.Variant != nil {
localProject.Variant = strings.TrimSpace(*req.Variant)
if err := validateProjectVariantName(localProject.Variant); err != nil {
return nil, err
}
}
if err := s.ensureUniqueProjectCodeVariant(projectUUID, localProject.Code, localProject.Variant); err != nil {
return nil, err
@@ -166,6 +173,13 @@ func normalizeProjectVariant(variant string) string {
return strings.ToLower(strings.TrimSpace(variant))
}
func validateProjectVariantName(variant string) error {
if normalizeProjectVariant(variant) == "main" {
return ErrReservedMainVariant
}
return nil
}
func (s *ProjectService) Archive(projectUUID, ownerUsername string) error {
return s.setProjectActive(projectUUID, ownerUsername, false)
}

View File

@@ -0,0 +1,60 @@
package services
import (
"errors"
"path/filepath"
"testing"
"git.mchus.pro/mchus/quoteforge/internal/localdb"
)
func TestProjectServiceCreateRejectsReservedMainVariant(t *testing.T) {
local, err := newProjectTestLocalDB(t)
if err != nil {
t.Fatalf("open localdb: %v", err)
}
service := NewProjectService(local)
_, err = service.Create("tester", &CreateProjectRequest{
Code: "OPS-1",
Variant: "main",
})
if !errors.Is(err, ErrReservedMainVariant) {
t.Fatalf("expected ErrReservedMainVariant, got %v", err)
}
}
func TestProjectServiceUpdateRejectsReservedMainVariant(t *testing.T) {
local, err := newProjectTestLocalDB(t)
if err != nil {
t.Fatalf("open localdb: %v", err)
}
service := NewProjectService(local)
created, err := service.Create("tester", &CreateProjectRequest{
Code: "OPS-1",
Variant: "Lenovo",
})
if err != nil {
t.Fatalf("create project: %v", err)
}
mainName := "main"
_, err = service.Update(created.UUID, "tester", &UpdateProjectRequest{
Variant: &mainName,
})
if !errors.Is(err, ErrReservedMainVariant) {
t.Fatalf("expected ErrReservedMainVariant, got %v", err)
}
}
func newProjectTestLocalDB(t *testing.T) (*localdb.LocalDB, error) {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "project_test.db")
local, err := localdb.New(dbPath)
if err != nil {
return nil, err
}
t.Cleanup(func() { _ = local.Close() })
return local, nil
}

View File

@@ -215,7 +215,7 @@ func (s *Service) ImportProjectsToLocal() (*ProjectImportResult, error) {
existing.SyncStatus = "synced"
existing.SyncedAt = &now
if err := s.localDB.SaveProject(existing); err != nil {
if err := s.localDB.SaveProjectPreservingUpdatedAt(existing); err != nil {
return nil, fmt.Errorf("saving local project %s: %w", project.UUID, err)
}
result.Updated++
@@ -225,7 +225,7 @@ func (s *Service) ImportProjectsToLocal() (*ProjectImportResult, error) {
localProject := localdb.ProjectToLocal(&project)
localProject.SyncStatus = "synced"
localProject.SyncedAt = &now
if err := s.localDB.SaveProject(localProject); err != nil {
if err := s.localDB.SaveProjectPreservingUpdatedAt(localProject); err != nil {
return nil, fmt.Errorf("saving local project %s: %w", project.UUID, err)
}
result.Imported++
@@ -1008,7 +1008,7 @@ func (s *Service) pushProjectChange(change *localdb.PendingChange) error {
localProject.SyncStatus = "synced"
now := time.Now()
localProject.SyncedAt = &now
_ = s.localDB.SaveProject(localProject)
_ = s.localDB.SaveProjectPreservingUpdatedAt(localProject)
}
return nil
@@ -1278,7 +1278,7 @@ func (s *Service) ensureConfigurationProject(mariaDB *gorm.DB, cfg *models.Confi
localProject.SyncStatus = "synced"
now := time.Now()
localProject.SyncedAt = &now
_ = s.localDB.SaveProject(localProject)
_ = s.localDB.SaveProjectPreservingUpdatedAt(localProject)
}
return nil
}