package services import ( "errors" "time" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/mchus/quoteforge/internal/repository" "github.com/google/uuid" ) var ( ErrConfigNotFound = errors.New("configuration not found") ErrConfigForbidden = errors.New("access to configuration forbidden") ) // ConfigurationGetter is an interface for services that can retrieve configurations // Used by handlers to work with both ConfigurationService and LocalConfigurationService type ConfigurationGetter interface { GetByUUID(uuid string, ownerUsername string) (*models.Configuration, error) } type ConfigurationService struct { configRepo *repository.ConfigurationRepository componentRepo *repository.ComponentRepository quoteService *QuoteService } func NewConfigurationService( configRepo *repository.ConfigurationRepository, componentRepo *repository.ComponentRepository, quoteService *QuoteService, ) *ConfigurationService { return &ConfigurationService{ configRepo: configRepo, componentRepo: componentRepo, quoteService: quoteService, } } type CreateConfigRequest struct { Name string `json:"name"` Items models.ConfigItems `json:"items"` CustomPrice *float64 `json:"custom_price"` Notes string `json:"notes"` IsTemplate bool `json:"is_template"` ServerCount int `json:"server_count"` } func (s *ConfigurationService) Create(ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) { total := req.Items.Total() // If server count is greater than 1, multiply the total by server count if req.ServerCount > 1 { total *= float64(req.ServerCount) } config := &models.Configuration{ UUID: uuid.New().String(), OwnerUsername: ownerUsername, Name: req.Name, Items: req.Items, TotalPrice: &total, CustomPrice: req.CustomPrice, Notes: req.Notes, IsTemplate: req.IsTemplate, ServerCount: req.ServerCount, } if err := s.configRepo.Create(config); err != nil { return nil, err } // Record usage stats _ = s.quoteService.RecordUsage(req.Items) return config, nil } func (s *ConfigurationService) GetByUUID(uuid string, ownerUsername string) (*models.Configuration, error) { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } // Allow access if user owns config or it's a template if !s.isOwner(config, ownerUsername) && !config.IsTemplate { return nil, ErrConfigForbidden } return config, nil } func (s *ConfigurationService) Update(uuid string, ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } if !s.isOwner(config, ownerUsername) { return nil, ErrConfigForbidden } total := req.Items.Total() // If server count is greater than 1, multiply the total by server count if req.ServerCount > 1 { total *= float64(req.ServerCount) } config.Name = req.Name config.Items = req.Items config.TotalPrice = &total config.CustomPrice = req.CustomPrice config.Notes = req.Notes config.IsTemplate = req.IsTemplate config.ServerCount = req.ServerCount if err := s.configRepo.Update(config); err != nil { return nil, err } return config, nil } func (s *ConfigurationService) Delete(uuid string, ownerUsername string) error { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return ErrConfigNotFound } if !s.isOwner(config, ownerUsername) { return ErrConfigForbidden } return s.configRepo.Delete(config.ID) } func (s *ConfigurationService) Rename(uuid string, ownerUsername string, newName string) (*models.Configuration, error) { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } if !s.isOwner(config, ownerUsername) { return nil, ErrConfigForbidden } config.Name = newName if err := s.configRepo.Update(config); err != nil { return nil, err } return config, nil } func (s *ConfigurationService) Clone(configUUID string, ownerUsername string, newName string) (*models.Configuration, error) { original, err := s.GetByUUID(configUUID, ownerUsername) if err != nil { return nil, err } // Create copy with new UUID and name total := original.Items.Total() // If server count is greater than 1, multiply the total by server count if original.ServerCount > 1 { total *= float64(original.ServerCount) } clone := &models.Configuration{ UUID: uuid.New().String(), OwnerUsername: ownerUsername, Name: newName, Items: original.Items, TotalPrice: &total, CustomPrice: original.CustomPrice, Notes: original.Notes, IsTemplate: false, // Clone is never a template ServerCount: original.ServerCount, } if err := s.configRepo.Create(clone); err != nil { return nil, err } return clone, nil } func (s *ConfigurationService) ListByUser(ownerUsername string, page, perPage int) ([]models.Configuration, int64, error) { if page < 1 { page = 1 } if perPage < 1 || perPage > 100 { perPage = 20 } offset := (page - 1) * perPage return s.configRepo.ListByUser(ownerUsername, offset, perPage) } // ListAll returns all configurations without user filter (for use when auth is disabled) func (s *ConfigurationService) ListAll(page, perPage int) ([]models.Configuration, int64, error) { if page < 1 { page = 1 } if perPage < 1 || perPage > 100 { perPage = 20 } offset := (page - 1) * perPage return s.configRepo.ListAll(offset, perPage) } // GetByUUIDNoAuth returns configuration without ownership check (for use when auth is disabled) func (s *ConfigurationService) GetByUUIDNoAuth(uuid string) (*models.Configuration, error) { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } return config, nil } // UpdateNoAuth updates configuration without ownership check func (s *ConfigurationService) UpdateNoAuth(uuid string, req *CreateConfigRequest) (*models.Configuration, error) { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } total := req.Items.Total() if req.ServerCount > 1 { total *= float64(req.ServerCount) } config.Name = req.Name config.Items = req.Items config.TotalPrice = &total config.CustomPrice = req.CustomPrice config.Notes = req.Notes config.IsTemplate = req.IsTemplate config.ServerCount = req.ServerCount if err := s.configRepo.Update(config); err != nil { return nil, err } return config, nil } // DeleteNoAuth deletes configuration without ownership check func (s *ConfigurationService) DeleteNoAuth(uuid string) error { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return ErrConfigNotFound } return s.configRepo.Delete(config.ID) } // RenameNoAuth renames configuration without ownership check func (s *ConfigurationService) RenameNoAuth(uuid string, newName string) (*models.Configuration, error) { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } config.Name = newName if err := s.configRepo.Update(config); err != nil { return nil, err } return config, nil } // CloneNoAuth clones configuration without ownership check func (s *ConfigurationService) CloneNoAuth(configUUID string, newName string, ownerUsername string) (*models.Configuration, error) { original, err := s.configRepo.GetByUUID(configUUID) if err != nil { return nil, ErrConfigNotFound } total := original.Items.Total() if original.ServerCount > 1 { total *= float64(original.ServerCount) } clone := &models.Configuration{ UUID: uuid.New().String(), OwnerUsername: ownerUsername, Name: newName, Items: original.Items, TotalPrice: &total, CustomPrice: original.CustomPrice, Notes: original.Notes, IsTemplate: false, ServerCount: original.ServerCount, } if err := s.configRepo.Create(clone); err != nil { return nil, err } return clone, nil } // RefreshPricesNoAuth refreshes prices without ownership check func (s *ConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Configuration, error) { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } updatedItems := make(models.ConfigItems, len(config.Items)) for i, item := range config.Items { metadata, err := s.componentRepo.GetByLotName(item.LotName) if err != nil || metadata.CurrentPrice == nil { updatedItems[i] = item continue } updatedItems[i] = models.ConfigItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: *metadata.CurrentPrice, } } config.Items = updatedItems total := updatedItems.Total() if config.ServerCount > 1 { total *= float64(config.ServerCount) } config.TotalPrice = &total now := time.Now() config.PriceUpdatedAt = &now if err := s.configRepo.Update(config); err != nil { return nil, err } return config, nil } func (s *ConfigurationService) ListTemplates(page, perPage int) ([]models.Configuration, int64, error) { if page < 1 { page = 1 } if perPage < 1 || perPage > 100 { perPage = 20 } offset := (page - 1) * perPage return s.configRepo.ListTemplates(offset, perPage) } // RefreshPrices updates all component prices in the configuration with current prices func (s *ConfigurationService) RefreshPrices(uuid string, ownerUsername string) (*models.Configuration, error) { config, err := s.configRepo.GetByUUID(uuid) if err != nil { return nil, ErrConfigNotFound } if !s.isOwner(config, ownerUsername) { return nil, ErrConfigForbidden } // Update prices for all items updatedItems := make(models.ConfigItems, len(config.Items)) for i, item := range config.Items { // Get current component price metadata, err := s.componentRepo.GetByLotName(item.LotName) if err != nil || metadata.CurrentPrice == nil { // Keep original item if component not found or no price available updatedItems[i] = item continue } // Update item with current price updatedItems[i] = models.ConfigItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: *metadata.CurrentPrice, } } // Update configuration config.Items = updatedItems total := updatedItems.Total() // If server count is greater than 1, multiply the total by server count if config.ServerCount > 1 { total *= float64(config.ServerCount) } config.TotalPrice = &total // Set price update timestamp now := time.Now() config.PriceUpdatedAt = &now if err := s.configRepo.Update(config); err != nil { return nil, err } return config, nil } func (s *ConfigurationService) isOwner(config *models.Configuration, ownerUsername string) bool { if config == nil || ownerUsername == "" { return false } if config.OwnerUsername != "" { return config.OwnerUsername == ownerUsername } if config.User != nil { return config.User.Username == ownerUsername } return false } // // Export configuration as JSON // type ConfigExport struct { // Name string `json:"name"` // Notes string `json:"notes"` // Items models.ConfigItems `json:"items"` // } // // func (s *ConfigurationService) ExportJSON(uuid string, userID uint) ([]byte, error) { // config, err := s.GetByUUID(uuid, userID) // if err != nil { // return nil, err // } // // export := ConfigExport{ // Name: config.Name, // Notes: config.Notes, // Items: config.Items, // } // // return json.MarshalIndent(export, "", " ") // } // // func (s *ConfigurationService) ImportJSON(userID uint, data []byte) (*models.Configuration, error) { // var export ConfigExport // if err := json.Unmarshal(data, &export); err != nil { // return nil, err // } // // req := &CreateConfigRequest{ // Name: export.Name, // Notes: export.Notes, // Items: export.Items, // } // // return s.Create(userID, req) // }