This commit is contained in:
2026-01-25 21:59:00 +08:00
parent 7fd537bef3
commit 4cad3f0250
118 changed files with 30473 additions and 0 deletions

View File

@@ -0,0 +1,323 @@
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
}
}