Files
Novault-backend/internal/service/ledger_service.go
2026-01-25 21:59:00 +08:00

260 lines
7.5 KiB
Go

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
}