Files
QuoteForge/internal/services/local_configuration.go
2026-02-04 11:31:23 +03:00

629 lines
16 KiB
Go

package services
import (
"encoding/json"
"time"
"git.mchus.pro/mchus/quoteforge/internal/localdb"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/services/sync"
"github.com/google/uuid"
)
// LocalConfigurationService handles configurations in local-first mode
// All operations go through SQLite, MariaDB is used only for sync
type LocalConfigurationService struct {
localDB *localdb.LocalDB
syncService *sync.Service
quoteService *QuoteService
isOnline func() bool // Function to check if we're online
}
// NewLocalConfigurationService creates a new local-first configuration service
func NewLocalConfigurationService(
localDB *localdb.LocalDB,
syncService *sync.Service,
quoteService *QuoteService,
isOnline func() bool,
) *LocalConfigurationService {
return &LocalConfigurationService{
localDB: localDB,
syncService: syncService,
quoteService: quoteService,
isOnline: isOnline,
}
}
// Create creates a new configuration in local SQLite and queues it for sync
func (s *LocalConfigurationService) Create(userID uint, req *CreateConfigRequest) (*models.Configuration, error) {
// If online, check for new pricelists first
if s.isOnline() {
if err := s.syncService.SyncPricelistsIfNeeded(); err != nil {
// Log but don't fail - we can still use local pricelists
}
}
total := req.Items.Total()
if req.ServerCount > 1 {
total *= float64(req.ServerCount)
}
cfg := &models.Configuration{
UUID: uuid.New().String(),
UserID: userID,
Name: req.Name,
Items: req.Items,
TotalPrice: &total,
CustomPrice: req.CustomPrice,
Notes: req.Notes,
IsTemplate: req.IsTemplate,
ServerCount: req.ServerCount,
CreatedAt: time.Now(),
}
// Convert to local model
localCfg := localdb.ConfigurationToLocal(cfg)
// Save to local SQLite
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
return nil, err
}
// Add to pending sync queue
payload, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
if err := s.localDB.AddPendingChange("configuration", cfg.UUID, "create", string(payload)); err != nil {
return nil, err
}
// Record usage stats
_ = s.quoteService.RecordUsage(req.Items)
return cfg, nil
}
// GetByUUID returns a configuration from local SQLite
func (s *LocalConfigurationService) GetByUUID(uuid string, userID uint) (*models.Configuration, error) {
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
if err != nil {
return nil, ErrConfigNotFound
}
// Convert to models.Configuration
cfg := localdb.LocalToConfiguration(localCfg)
// Allow access if user owns config or it's a template
if cfg.UserID != userID && !cfg.IsTemplate {
return nil, ErrConfigForbidden
}
return cfg, nil
}
// Update updates a configuration in local SQLite and queues it for sync
func (s *LocalConfigurationService) Update(uuid string, userID uint, req *CreateConfigRequest) (*models.Configuration, error) {
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
if err != nil {
return nil, ErrConfigNotFound
}
if localCfg.OriginalUserID != userID {
return nil, ErrConfigForbidden
}
total := req.Items.Total()
if req.ServerCount > 1 {
total *= float64(req.ServerCount)
}
// Update fields
localCfg.Name = req.Name
localCfg.Items = localdb.LocalConfigItems{}
for _, item := range req.Items {
localCfg.Items = append(localCfg.Items, localdb.LocalConfigItem{
LotName: item.LotName,
Quantity: item.Quantity,
UnitPrice: item.UnitPrice,
})
}
localCfg.TotalPrice = &total
localCfg.CustomPrice = req.CustomPrice
localCfg.Notes = req.Notes
localCfg.IsTemplate = req.IsTemplate
localCfg.ServerCount = req.ServerCount
localCfg.UpdatedAt = time.Now()
localCfg.SyncStatus = "pending"
// Save to local SQLite
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
return nil, err
}
// Add to pending sync queue
cfg := localdb.LocalToConfiguration(localCfg)
payload, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
return nil, err
}
return cfg, nil
}
// Delete deletes a configuration from local SQLite and queues it for sync
func (s *LocalConfigurationService) Delete(uuid string, userID uint) error {
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
if err != nil {
return ErrConfigNotFound
}
if localCfg.OriginalUserID != userID {
return ErrConfigForbidden
}
// Delete from local SQLite
if err := s.localDB.DeleteConfiguration(uuid); err != nil {
return err
}
// Add to pending sync queue
if err := s.localDB.AddPendingChange("configuration", uuid, "delete", ""); err != nil {
return err
}
return nil
}
// Rename renames a configuration
func (s *LocalConfigurationService) Rename(uuid string, userID uint, newName string) (*models.Configuration, error) {
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
if err != nil {
return nil, ErrConfigNotFound
}
if localCfg.OriginalUserID != userID {
return nil, ErrConfigForbidden
}
localCfg.Name = newName
localCfg.UpdatedAt = time.Now()
localCfg.SyncStatus = "pending"
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
return nil, err
}
// Add to pending sync queue
cfg := localdb.LocalToConfiguration(localCfg)
payload, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
return nil, err
}
return cfg, nil
}
// Clone clones a configuration
func (s *LocalConfigurationService) Clone(configUUID string, userID uint, newName string) (*models.Configuration, error) {
original, err := s.GetByUUID(configUUID, userID)
if err != nil {
return nil, err
}
total := original.Items.Total()
if original.ServerCount > 1 {
total *= float64(original.ServerCount)
}
clone := &models.Configuration{
UUID: uuid.New().String(),
UserID: userID,
Name: newName,
Items: original.Items,
TotalPrice: &total,
CustomPrice: original.CustomPrice,
Notes: original.Notes,
IsTemplate: false,
ServerCount: original.ServerCount,
CreatedAt: time.Now(),
}
localCfg := localdb.ConfigurationToLocal(clone)
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
return nil, err
}
// Add to pending sync queue
payload, err := json.Marshal(clone)
if err != nil {
return nil, err
}
if err := s.localDB.AddPendingChange("configuration", clone.UUID, "create", string(payload)); err != nil {
return nil, err
}
return clone, nil
}
// ListByUser returns all configurations for a user from local SQLite
func (s *LocalConfigurationService) ListByUser(userID uint, page, perPage int) ([]models.Configuration, int64, error) {
// Get all local configurations
localConfigs, err := s.localDB.GetConfigurations()
if err != nil {
return nil, 0, err
}
// Filter by user
var userConfigs []models.Configuration
for _, lc := range localConfigs {
if lc.OriginalUserID == userID || lc.IsTemplate {
userConfigs = append(userConfigs, *localdb.LocalToConfiguration(&lc))
}
}
total := int64(len(userConfigs))
// Apply pagination
if page < 1 {
page = 1
}
if perPage < 1 || perPage > 100 {
perPage = 20
}
offset := (page - 1) * perPage
start := offset
if start > len(userConfigs) {
start = len(userConfigs)
}
end := start + perPage
if end > len(userConfigs) {
end = len(userConfigs)
}
return userConfigs[start:end], total, nil
}
// RefreshPrices updates all component prices in the configuration from local cache
func (s *LocalConfigurationService) RefreshPrices(uuid string, userID uint) (*models.Configuration, error) {
// Get configuration from local SQLite
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
if err != nil {
return nil, ErrConfigNotFound
}
// Check ownership
if localCfg.OriginalUserID != userID {
return nil, ErrConfigForbidden
}
// Update prices for all items
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
for i, item := range localCfg.Items {
// Get current component price from local cache
component, err := s.localDB.GetLocalComponent(item.LotName)
if err != nil || component.CurrentPrice == nil {
// Keep original item if component not found or no price available
updatedItems[i] = item
continue
}
// Update item with current price from local cache
updatedItems[i] = localdb.LocalConfigItem{
LotName: item.LotName,
Quantity: item.Quantity,
UnitPrice: *component.CurrentPrice,
}
}
// Update configuration
localCfg.Items = updatedItems
total := updatedItems.Total()
// If server count is greater than 1, multiply the total by server count
if localCfg.ServerCount > 1 {
total *= float64(localCfg.ServerCount)
}
localCfg.TotalPrice = &total
// Set price update timestamp and mark for sync
now := time.Now()
localCfg.PriceUpdatedAt = &now
localCfg.UpdatedAt = now
localCfg.SyncStatus = "pending"
// Save to local SQLite
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
return nil, err
}
// Add to pending sync queue
cfg := localdb.LocalToConfiguration(localCfg)
payload, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
return nil, err
}
return cfg, nil
}
// GetByUUIDNoAuth returns configuration without ownership check
func (s *LocalConfigurationService) GetByUUIDNoAuth(uuid string) (*models.Configuration, error) {
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
if err != nil {
return nil, ErrConfigNotFound
}
return localdb.LocalToConfiguration(localCfg), nil
}
// UpdateNoAuth updates configuration without ownership check
func (s *LocalConfigurationService) UpdateNoAuth(uuid string, req *CreateConfigRequest) (*models.Configuration, error) {
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
if err != nil {
return nil, ErrConfigNotFound
}
total := req.Items.Total()
if req.ServerCount > 1 {
total *= float64(req.ServerCount)
}
localCfg.Name = req.Name
localCfg.Items = localdb.LocalConfigItems{}
for _, item := range req.Items {
localCfg.Items = append(localCfg.Items, localdb.LocalConfigItem{
LotName: item.LotName,
Quantity: item.Quantity,
UnitPrice: item.UnitPrice,
})
}
localCfg.TotalPrice = &total
localCfg.CustomPrice = req.CustomPrice
localCfg.Notes = req.Notes
localCfg.IsTemplate = req.IsTemplate
localCfg.ServerCount = req.ServerCount
localCfg.UpdatedAt = time.Now()
localCfg.SyncStatus = "pending"
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
return nil, err
}
cfg := localdb.LocalToConfiguration(localCfg)
payload, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
return nil, err
}
return cfg, nil
}
// DeleteNoAuth deletes configuration without ownership check
func (s *LocalConfigurationService) DeleteNoAuth(uuid string) error {
if err := s.localDB.DeleteConfiguration(uuid); err != nil {
return err
}
return s.localDB.AddPendingChange("configuration", uuid, "delete", "")
}
// RenameNoAuth renames configuration without ownership check
func (s *LocalConfigurationService) RenameNoAuth(uuid string, newName string) (*models.Configuration, error) {
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
if err != nil {
return nil, ErrConfigNotFound
}
localCfg.Name = newName
localCfg.UpdatedAt = time.Now()
localCfg.SyncStatus = "pending"
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
return nil, err
}
cfg := localdb.LocalToConfiguration(localCfg)
payload, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
return nil, err
}
return cfg, nil
}
// CloneNoAuth clones configuration without ownership check
func (s *LocalConfigurationService) CloneNoAuth(configUUID string, newName string, userID uint) (*models.Configuration, error) {
original, err := s.GetByUUIDNoAuth(configUUID)
if err != nil {
return nil, err
}
total := original.Items.Total()
if original.ServerCount > 1 {
total *= float64(original.ServerCount)
}
clone := &models.Configuration{
UUID: uuid.New().String(),
UserID: userID,
Name: newName,
Items: original.Items,
TotalPrice: &total,
CustomPrice: original.CustomPrice,
Notes: original.Notes,
IsTemplate: false,
ServerCount: original.ServerCount,
CreatedAt: time.Now(),
}
localCfg := localdb.ConfigurationToLocal(clone)
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
return nil, err
}
payload, err := json.Marshal(clone)
if err != nil {
return nil, err
}
if err := s.localDB.AddPendingChange("configuration", clone.UUID, "create", string(payload)); err != nil {
return nil, err
}
return clone, nil
}
// ListAll returns all configurations without user filter
func (s *LocalConfigurationService) ListAll(page, perPage int) ([]models.Configuration, int64, error) {
localConfigs, err := s.localDB.GetConfigurations()
if err != nil {
return nil, 0, err
}
configs := make([]models.Configuration, len(localConfigs))
for i, lc := range localConfigs {
configs[i] = *localdb.LocalToConfiguration(&lc)
}
total := int64(len(configs))
// Apply pagination
if page < 1 {
page = 1
}
if perPage < 1 || perPage > 100 {
perPage = 20
}
offset := (page - 1) * perPage
start := offset
if start > len(configs) {
start = len(configs)
}
end := start + perPage
if end > len(configs) {
end = len(configs)
}
return configs[start:end], total, nil
}
// ListTemplates returns all template configurations
func (s *LocalConfigurationService) ListTemplates(page, perPage int) ([]models.Configuration, int64, error) {
localConfigs, err := s.localDB.GetConfigurations()
if err != nil {
return nil, 0, err
}
var templates []models.Configuration
for _, lc := range localConfigs {
if lc.IsTemplate {
templates = append(templates, *localdb.LocalToConfiguration(&lc))
}
}
total := int64(len(templates))
// Apply pagination
if page < 1 {
page = 1
}
if perPage < 1 || perPage > 100 {
perPage = 20
}
offset := (page - 1) * perPage
start := offset
if start > len(templates) {
start = len(templates)
}
end := start + perPage
if end > len(templates) {
end = len(templates)
}
return templates[start:end], total, nil
}
// RefreshPricesNoAuth updates all component prices in the configuration without ownership check
func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Configuration, error) {
// Get configuration from local SQLite
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
if err != nil {
return nil, ErrConfigNotFound
}
// Update prices for all items
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
for i, item := range localCfg.Items {
// Get current component price from local cache
component, err := s.localDB.GetLocalComponent(item.LotName)
if err != nil || component.CurrentPrice == nil {
// Keep original item if component not found or no price available
updatedItems[i] = item
continue
}
// Update item with current price from local cache
updatedItems[i] = localdb.LocalConfigItem{
LotName: item.LotName,
Quantity: item.Quantity,
UnitPrice: *component.CurrentPrice,
}
}
// Update configuration
localCfg.Items = updatedItems
total := updatedItems.Total()
// If server count is greater than 1, multiply the total by server count
if localCfg.ServerCount > 1 {
total *= float64(localCfg.ServerCount)
}
localCfg.TotalPrice = &total
// Set price update timestamp and mark for sync
now := time.Now()
localCfg.PriceUpdatedAt = &now
localCfg.UpdatedAt = now
localCfg.SyncStatus = "pending"
// Save to local SQLite
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
return nil, err
}
// Add to pending sync queue
cfg := localdb.LocalToConfiguration(localCfg)
payload, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
return nil, err
}
return cfg, nil
}
// ImportFromServer imports configurations from MariaDB to local SQLite cache.
func (s *LocalConfigurationService) ImportFromServer() (*sync.ConfigImportResult, error) {
return s.syncService.ImportConfigurationsToLocal()
}