package service import ( "errors" "fmt" "accounting-app/internal/models" "accounting-app/internal/repository" "gorm.io/gorm" ) // Service layer errors for ledgers var ( ErrLedgerNotFound = errors.New("ledger not found") ErrLedgerLimitExceeded = errors.New("maximum number of ledgers exceeded") ErrCannotDeleteLastLedger = errors.New("cannot delete the last ledger") ErrInvalidTheme = errors.New("invalid theme, must be one of: pink, beige, brown") ) // LedgerInput represents the input data for creating or updating a ledger type LedgerInput struct { Name string `json:"name" binding:"required,max=100"` Theme string `json:"theme" binding:"omitempty,oneof=pink beige brown"` CoverImage string `json:"cover_image"` IsDefault bool `json:"is_default"` SortOrder int `json:"sort_order"` } // LedgerServiceInterface defines the interface for ledger service operations type LedgerServiceInterface interface { CreateLedger(userID uint, input LedgerInput) (*models.Ledger, error) GetLedger(userID uint, id uint) (*models.Ledger, error) GetAllLedgers(userID uint) ([]models.Ledger, error) UpdateLedger(userID uint, id uint, input LedgerInput) (*models.Ledger, error) DeleteLedger(userID uint, id uint) error GetDefaultLedger(userID uint) (*models.Ledger, error) GetDeletedLedgers(userID uint) ([]models.Ledger, error) RestoreLedger(userID uint, id uint) error } // LedgerService handles business logic for ledgers type LedgerService struct { repo *repository.LedgerRepository db *gorm.DB } // NewLedgerService creates a new LedgerService instance func NewLedgerService(repo *repository.LedgerRepository, db *gorm.DB) *LedgerService { return &LedgerService{ repo: repo, db: db, } } // CreateLedger creates a new ledger with business logic validation // Feature: accounting-feature-upgrade // Validates: Requirements 3.1-3.6, 3.12 func (s *LedgerService) CreateLedger(userID uint, input LedgerInput) (*models.Ledger, error) { // Check if the ledger limit has been reached count, err := s.repo.Count(userID) if err != nil { return nil, fmt.Errorf("failed to count ledgers: %w", err) } if count >= models.MaxLedgersPerUser { return nil, ErrLedgerLimitExceeded } // Validate theme if provided if input.Theme != "" && input.Theme != "pink" && input.Theme != "beige" && input.Theme != "brown" { return nil, ErrInvalidTheme } // Create the ledger model ledger := &models.Ledger{ UserID: userID, Name: input.Name, Theme: input.Theme, CoverImage: input.CoverImage, IsDefault: input.IsDefault, SortOrder: input.SortOrder, } // If this is set as default, we need to unset other defaults if input.IsDefault { if err := s.repo.SetDefault(userID, 0); err != nil { return nil, fmt.Errorf("failed to unset default ledgers: %w", err) } } // Save to database if err := s.repo.Create(ledger); err != nil { return nil, fmt.Errorf("failed to create ledger: %w", err) } // If this is the first ledger, set it as default if count == 0 { ledger.IsDefault = true if err := s.repo.Update(userID, ledger); err != nil { return nil, fmt.Errorf("failed to set first ledger as default: %w", err) } } return ledger, nil } // GetLedger retrieves a ledger by ID func (s *LedgerService) GetLedger(userID uint, id uint) (*models.Ledger, error) { ledger, err := s.repo.GetByID(userID, id) if err != nil { if errors.Is(err, repository.ErrLedgerNotFound) { return nil, ErrLedgerNotFound } return nil, fmt.Errorf("failed to get ledger: %w", err) } return ledger, nil } // GetAllLedgers retrieves all ledgers func (s *LedgerService) GetAllLedgers(userID uint) ([]models.Ledger, error) { ledgers, err := s.repo.GetAll(userID) if err != nil { return nil, fmt.Errorf("failed to get ledgers: %w", err) } return ledgers, nil } // UpdateLedger updates an existing ledger // Feature: accounting-feature-upgrade // Validates: Requirements 3.6 func (s *LedgerService) UpdateLedger(userID uint, id uint, input LedgerInput) (*models.Ledger, error) { // Get existing ledger ledger, err := s.repo.GetByID(userID, id) if err != nil { if errors.Is(err, repository.ErrLedgerNotFound) { return nil, ErrLedgerNotFound } return nil, fmt.Errorf("failed to get ledger: %w", err) } // Validate theme if provided if input.Theme != "" && input.Theme != "pink" && input.Theme != "beige" && input.Theme != "brown" { return nil, ErrInvalidTheme } // Update fields ledger.Name = input.Name if input.Theme != "" { ledger.Theme = input.Theme } ledger.CoverImage = input.CoverImage ledger.SortOrder = input.SortOrder // Handle default status change if input.IsDefault && !ledger.IsDefault { // Setting this ledger as default if err := s.repo.SetDefault(userID, id); err != nil { return nil, fmt.Errorf("failed to set default ledger: %w", err) } ledger.IsDefault = true } // Save to database if err := s.repo.Update(userID, ledger); err != nil { return nil, fmt.Errorf("failed to update ledger: %w", err) } return ledger, nil } // DeleteLedger soft-deletes a ledger by ID // Feature: accounting-feature-upgrade // Validates: Requirements 3.7, 3.8, 3.15 func (s *LedgerService) DeleteLedger(userID uint, id uint) error { // Check if this is the last ledger count, err := s.repo.Count(userID) if err != nil { return fmt.Errorf("failed to count ledgers: %w", err) } if count <= 1 { return ErrCannotDeleteLastLedger } // Get the ledger to check if it's the default ledger, err := s.repo.GetByID(userID, id) if err != nil { if errors.Is(err, repository.ErrLedgerNotFound) { return ErrLedgerNotFound } return fmt.Errorf("failed to get ledger: %w", err) } // Delete the ledger if err := s.repo.Delete(userID, id); err != nil { return fmt.Errorf("failed to delete ledger: %w", err) } // If this was the default ledger, set the first remaining ledger as default if ledger.IsDefault { ledgers, err := s.repo.GetAll(userID) if err != nil { return fmt.Errorf("failed to get ledgers after deletion: %w", err) } if len(ledgers) > 0 { if err := s.repo.SetDefault(userID, ledgers[0].ID); err != nil { return fmt.Errorf("failed to set new default ledger: %w", err) } } } return nil } // GetDefaultLedger retrieves the default ledger func (s *LedgerService) GetDefaultLedger(userID uint) (*models.Ledger, error) { ledger, err := s.repo.GetDefault(userID) if err != nil { if errors.Is(err, repository.ErrLedgerNotFound) { return nil, ErrLedgerNotFound } return nil, fmt.Errorf("failed to get default ledger: %w", err) } return ledger, nil } // GetDeletedLedgers retrieves all soft-deleted ledgers // Feature: accounting-feature-upgrade // Validates: Requirements 3.9 func (s *LedgerService) GetDeletedLedgers(userID uint) ([]models.Ledger, error) { ledgers, err := s.repo.GetDeleted(userID) if err != nil { return nil, fmt.Errorf("failed to get deleted ledgers: %w", err) } return ledgers, nil } // RestoreLedger restores a soft-deleted ledger by ID // Feature: accounting-feature-upgrade // Validates: Requirements 3.9 func (s *LedgerService) RestoreLedger(userID uint, id uint) error { // Check if restoring would exceed the ledger limit count, err := s.repo.Count(userID) if err != nil { return fmt.Errorf("failed to count ledgers: %w", err) } if count >= models.MaxLedgersPerUser { return ErrLedgerLimitExceeded } // Restore the ledger if err := s.repo.Restore(userID, id); err != nil { if errors.Is(err, repository.ErrLedgerNotFound) { return ErrLedgerNotFound } return fmt.Errorf("failed to restore ledger: %w", err) } return nil }