package service import ( "errors" "fmt" "accounting-app/internal/models" ) // Service layer errors for user settings var ( ErrInvalidIconLayout = errors.New("invalid icon layout, must be one of: four, five, six") ErrInvalidImageCompression = errors.New("invalid image compression, must be one of: low, medium, high") ErrDefaultAccountNotFound = errors.New("default account not found") ErrInvalidDefaultAccount = errors.New("invalid default account") ) // UserSettingsRepositoryInterface defines the interface for user settings repository operations type UserSettingsRepositoryInterface interface { GetOrCreate(userID uint) (*models.UserSettings, error) Update(settings *models.UserSettings) error GetWithDefaultAccounts(userID uint) (*models.UserSettings, error) } // AccountRepositoryInterface defines the interface for account repository operations needed by settings service type AccountRepositoryInterface interface { GetByID(userID uint, id uint) (*models.Account, error) ExistsByID(userID uint, id uint) (bool, error) } // UserSettingsInput represents the input data for updating user settings type UserSettingsInput struct { PreciseTimeEnabled *bool `json:"precise_time_enabled"` IconLayout *string `json:"icon_layout"` ImageCompression *string `json:"image_compression"` ShowReimbursementBtn *bool `json:"show_reimbursement_btn"` ShowRefundBtn *bool `json:"show_refund_btn"` CurrentLedgerID *uint `json:"current_ledger_id"` } // DefaultAccountsInput represents the input data for updating default accounts // Feature: financial-core-upgrade // Validates: Requirements 5.1, 5.2 type DefaultAccountsInput struct { DefaultExpenseAccountID *uint `json:"default_expense_account_id"` DefaultIncomeAccountID *uint `json:"default_income_account_id"` } // DefaultAccountsResponse represents the response for default accounts // Feature: financial-core-upgrade // Validates: Requirements 5.1, 5.2 type DefaultAccountsResponse struct { DefaultExpenseAccountID *uint `json:"default_expense_account_id,omitempty"` DefaultIncomeAccountID *uint `json:"default_income_account_id,omitempty"` DefaultExpenseAccount *models.Account `json:"default_expense_account,omitempty"` DefaultIncomeAccount *models.Account `json:"default_income_account,omitempty"` } // UserSettingsServiceInterface defines the interface for user settings service operations type UserSettingsServiceInterface interface { GetSettings(userID uint) (*models.UserSettings, error) UpdateSettings(userID uint, input UserSettingsInput) (*models.UserSettings, error) GetDefaultAccounts(userID uint) (*DefaultAccountsResponse, error) UpdateDefaultAccounts(userID uint, input DefaultAccountsInput) (*DefaultAccountsResponse, error) ClearDefaultAccount(userID uint, accountID uint) error } // UserSettingsService handles business logic for user settings type UserSettingsService struct { repo UserSettingsRepositoryInterface accountRepo AccountRepositoryInterface } // NewUserSettingsService creates a new UserSettingsService instance func NewUserSettingsService(repo UserSettingsRepositoryInterface) *UserSettingsService { return &UserSettingsService{ repo: repo, } } // NewUserSettingsServiceWithAccountRepo creates a new UserSettingsService instance with account repository // Feature: financial-core-upgrade // Validates: Requirements 5.1, 5.2, 5.6, 5.7, 5.8 func NewUserSettingsServiceWithAccountRepo(repo UserSettingsRepositoryInterface, accountRepo AccountRepositoryInterface) *UserSettingsService { return &UserSettingsService{ repo: repo, accountRepo: accountRepo, } } // GetSettings retrieves user settings, creating default settings if not found // Feature: accounting-feature-upgrade // Validates: Requirements 5.4, 6.5, 8.25-8.27 func (s *UserSettingsService) GetSettings(userID uint) (*models.UserSettings, error) { settings, err := s.repo.GetOrCreate(userID) if err != nil { return nil, fmt.Errorf("failed to get settings: %w", err) } return settings, nil } // UpdateSettings updates user settings with validation // Feature: accounting-feature-upgrade // Validates: Requirements 5.4, 6.5, 8.25-8.27 func (s *UserSettingsService) UpdateSettings(userID uint, input UserSettingsInput) (*models.UserSettings, error) { // Get existing settings settings, err := s.repo.GetOrCreate(userID) if err != nil { return nil, fmt.Errorf("failed to get settings: %w", err) } // Validate and update icon layout if provided if input.IconLayout != nil { layout := *input.IconLayout if layout != string(models.IconLayoutFour) && layout != string(models.IconLayoutFive) && layout != string(models.IconLayoutSix) { return nil, ErrInvalidIconLayout } settings.IconLayout = layout } // Validate and update image compression if provided if input.ImageCompression != nil { compression := *input.ImageCompression if compression != string(models.ImageCompressionLow) && compression != string(models.ImageCompressionMedium) && compression != string(models.ImageCompressionHigh) { return nil, ErrInvalidImageCompression } settings.ImageCompression = compression } // Update other fields if provided if input.PreciseTimeEnabled != nil { settings.PreciseTimeEnabled = *input.PreciseTimeEnabled } if input.ShowReimbursementBtn != nil { settings.ShowReimbursementBtn = *input.ShowReimbursementBtn } if input.ShowRefundBtn != nil { settings.ShowRefundBtn = *input.ShowRefundBtn } if input.CurrentLedgerID != nil { settings.CurrentLedgerID = input.CurrentLedgerID } // Save to database if err := s.repo.Update(settings); err != nil { return nil, fmt.Errorf("failed to update settings: %w", err) } return settings, nil } // GetDefaultAccounts retrieves the current default account settings // Feature: financial-core-upgrade // Validates: Requirements 5.1, 5.2 func (s *UserSettingsService) GetDefaultAccounts(userID uint) (*DefaultAccountsResponse, error) { settings, err := s.repo.GetOrCreate(userID) if err != nil { return nil, fmt.Errorf("failed to get settings: %w", err) } response := &DefaultAccountsResponse{ DefaultExpenseAccountID: settings.DefaultExpenseAccountID, DefaultIncomeAccountID: settings.DefaultIncomeAccountID, } // Load account details if account repo is available if s.accountRepo != nil { if settings.DefaultExpenseAccountID != nil { account, err := s.accountRepo.GetByID(userID, *settings.DefaultExpenseAccountID) if err == nil { response.DefaultExpenseAccount = account } } if settings.DefaultIncomeAccountID != nil { account, err := s.accountRepo.GetByID(userID, *settings.DefaultIncomeAccountID) if err == nil { response.DefaultIncomeAccount = account } } } return response, nil } // UpdateDefaultAccounts updates the default account settings // Feature: financial-core-upgrade // Validates: Requirements 5.1, 5.2, 5.7, 5.8 func (s *UserSettingsService) UpdateDefaultAccounts(userID uint, input DefaultAccountsInput) (*DefaultAccountsResponse, error) { // Get existing settings settings, err := s.repo.GetOrCreate(userID) if err != nil { return nil, fmt.Errorf("failed to get settings: %w", err) } // Validate and update default expense account if provided if input.DefaultExpenseAccountID != nil { if *input.DefaultExpenseAccountID == 0 { // Clear the default expense account settings.DefaultExpenseAccountID = nil } else { // Validate account exists if s.accountRepo != nil { exists, err := s.accountRepo.ExistsByID(userID, *input.DefaultExpenseAccountID) if err != nil { return nil, fmt.Errorf("failed to validate expense account: %w", err) } if !exists { return nil, ErrDefaultAccountNotFound } } settings.DefaultExpenseAccountID = input.DefaultExpenseAccountID } } // Validate and update default income account if provided if input.DefaultIncomeAccountID != nil { if *input.DefaultIncomeAccountID == 0 { // Clear the default income account settings.DefaultIncomeAccountID = nil } else { // Validate account exists if s.accountRepo != nil { exists, err := s.accountRepo.ExistsByID(userID, *input.DefaultIncomeAccountID) if err != nil { return nil, fmt.Errorf("failed to validate income account: %w", err) } if !exists { return nil, ErrDefaultAccountNotFound } } settings.DefaultIncomeAccountID = input.DefaultIncomeAccountID } } // Save to database if err := s.repo.Update(settings); err != nil { return nil, fmt.Errorf("failed to update settings: %w", err) } // Build response with account details response := &DefaultAccountsResponse{ DefaultExpenseAccountID: settings.DefaultExpenseAccountID, DefaultIncomeAccountID: settings.DefaultIncomeAccountID, } // Load account details if account repo is available if s.accountRepo != nil { if settings.DefaultExpenseAccountID != nil { account, err := s.accountRepo.GetByID(userID, *settings.DefaultExpenseAccountID) if err == nil { response.DefaultExpenseAccount = account } } if settings.DefaultIncomeAccountID != nil { account, err := s.accountRepo.GetByID(userID, *settings.DefaultIncomeAccountID) if err == nil { response.DefaultIncomeAccount = account } } } return response, nil } // ClearDefaultAccount clears the default account setting when an account is deleted // This should be called when an account is deleted to maintain data consistency // Feature: financial-core-upgrade // Validates: Requirements 5.6 func (s *UserSettingsService) ClearDefaultAccount(userID uint, accountID uint) error { settings, err := s.repo.GetOrCreate(userID) if err != nil { return fmt.Errorf("failed to get settings: %w", err) } updated := false // Clear default expense account if it matches the deleted account if settings.DefaultExpenseAccountID != nil && *settings.DefaultExpenseAccountID == accountID { settings.DefaultExpenseAccountID = nil updated = true } // Clear default income account if it matches the deleted account if settings.DefaultIncomeAccountID != nil && *settings.DefaultIncomeAccountID == accountID { settings.DefaultIncomeAccountID = nil updated = true } // Only update if changes were made if updated { if err := s.repo.Update(settings); err != nil { return fmt.Errorf("failed to clear default account: %w", err) } } return nil } // GetDefaultAccountForType returns the default account ID for a given transaction type // Feature: financial-core-upgrade // Validates: Requirements 5.3, 5.4, 5.5 func (s *UserSettingsService) GetDefaultAccountForType(userID uint, transactionType string) (*uint, error) { settings, err := s.repo.GetOrCreate(userID) if err != nil { return nil, fmt.Errorf("failed to get settings: %w", err) } switch transactionType { case "expense": return settings.DefaultExpenseAccountID, nil case "income": return settings.DefaultIncomeAccountID, nil default: return nil, nil } }