package service import ( "errors" "fmt" "time" "accounting-app/internal/models" "accounting-app/internal/repository" "gorm.io/gorm" ) // SavingsPotOperationResult represents the result of a savings pot operation type SavingsPotOperationResult struct { SavingsPot models.Account `json:"savings_pot"` MainAccount models.Account `json:"main_account"` TransactionID uint `json:"transaction_id"` } // SavingsPotDetail represents detailed savings pot information type SavingsPotDetail struct { models.Account Progress float64 `json:"progress"` // percentage towards target DaysRemaining *int `json:"days_remaining"` // days until target date } // SavingsPotService handles business logic for savings pot operations // Feature: financial-core-upgrade // Validates: Requirements 2.1-2.6, 2.8, 2.9, 16.1, 16.2 type SavingsPotService struct { repo *repository.AccountRepository transactionRepo *repository.TransactionRepository db *gorm.DB } // NewSavingsPotService creates a new SavingsPotService instance func NewSavingsPotService(repo *repository.AccountRepository, transactionRepo *repository.TransactionRepository, db *gorm.DB) *SavingsPotService { return &SavingsPotService{ repo: repo, transactionRepo: transactionRepo, db: db, } } // Deposit transfers money from main account to savings pot // Validates: Requirements 2.1-2.3, 2.8 func (s *SavingsPotService) Deposit(userID uint, savingsPotID uint, amount float64) (*SavingsPotOperationResult, error) { if amount <= 0 { return nil, ErrInvalidTransferAmount } var result SavingsPotOperationResult err := s.db.Transaction(func(tx *gorm.DB) error { // Get savings pot and verify ownership var savingsPot models.Account if err := tx.Where("id = ? AND user_id = ?", savingsPotID, userID).First(&savingsPot).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrSavingsPotNotFound } return fmt.Errorf("failed to get savings pot: %w", err) } // Verify it's a savings pot if savingsPot.SubAccountType == nil || *savingsPot.SubAccountType != models.SubAccountTypeSavingsPot { return errors.New("account is not a savings pot") } // Get parent account if savingsPot.ParentAccountID == nil { return errors.New("savings pot has no parent account") } var mainAccount models.Account if err := tx.First(&mainAccount, *savingsPot.ParentAccountID).Error; err != nil { return fmt.Errorf("failed to get main account: %w", err) } // Check if main account has enough available balance if mainAccount.AvailableBalance < amount { return ErrSavingsPotDepositLimit } // Update balances mainAccount.AvailableBalance -= amount mainAccount.FrozenBalance += amount savingsPot.Balance += amount // Save main account if err := tx.Save(&mainAccount).Error; err != nil { return fmt.Errorf("failed to update main account: %w", err) } // Save savings pot if err := tx.Save(&savingsPot).Error; err != nil { return fmt.Errorf("failed to update savings pot: %w", err) } // Create transaction record subType := models.TransactionSubTypeSavingsDeposit transaction := &models.Transaction{ Amount: amount, Type: models.TransactionTypeTransfer, CategoryID: 1, // Default category, should be configured AccountID: *savingsPot.ParentAccountID, ToAccountID: &savingsPotID, Currency: savingsPot.Currency, TransactionDate: time.Now(), SubType: &subType, Note: fmt.Sprintf("瀛樺叆瀛橀挶缃? %s", savingsPot.Name), } if err := tx.Create(transaction).Error; err != nil { return fmt.Errorf("failed to create transaction: %w", err) } result.SavingsPot = savingsPot result.MainAccount = mainAccount result.TransactionID = transaction.ID return nil }) if err != nil { return nil, err } return &result, nil } // Withdraw transfers money from savings pot back to main account // Validates: Requirements 2.4-2.6, 2.9 func (s *SavingsPotService) Withdraw(userID uint, savingsPotID uint, amount float64) (*SavingsPotOperationResult, error) { if amount <= 0 { return nil, ErrInvalidTransferAmount } var result SavingsPotOperationResult err := s.db.Transaction(func(tx *gorm.DB) error { // Get savings pot and verify ownership var savingsPot models.Account if err := tx.Where("id = ? AND user_id = ?", savingsPotID, userID).First(&savingsPot).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrSavingsPotNotFound } return fmt.Errorf("failed to get savings pot: %w", err) } // Verify it's a savings pot if savingsPot.SubAccountType == nil || *savingsPot.SubAccountType != models.SubAccountTypeSavingsPot { return errors.New("account is not a savings pot") } // Check if savings pot has enough balance if savingsPot.Balance < amount { return ErrSavingsPotWithdrawLimit } // Get parent account if savingsPot.ParentAccountID == nil { return errors.New("savings pot has no parent account") } var mainAccount models.Account if err := tx.First(&mainAccount, *savingsPot.ParentAccountID).Error; err != nil { return fmt.Errorf("failed to get main account: %w", err) } // Update balances savingsPot.Balance -= amount mainAccount.FrozenBalance -= amount mainAccount.AvailableBalance += amount // Save savings pot if err := tx.Save(&savingsPot).Error; err != nil { return fmt.Errorf("failed to update savings pot: %w", err) } // Save main account if err := tx.Save(&mainAccount).Error; err != nil { return fmt.Errorf("failed to update main account: %w", err) } // Create transaction record subType := models.TransactionSubTypeSavingsWithdraw transaction := &models.Transaction{ Amount: amount, Type: models.TransactionTypeTransfer, CategoryID: 1, // Default category, should be configured AccountID: savingsPotID, ToAccountID: savingsPot.ParentAccountID, Currency: savingsPot.Currency, TransactionDate: time.Now(), SubType: &subType, Note: fmt.Sprintf("浠庡瓨閽辩綈鍙栧嚭: %s", savingsPot.Name), } if err := tx.Create(transaction).Error; err != nil { return fmt.Errorf("failed to create transaction: %w", err) } result.SavingsPot = savingsPot result.MainAccount = mainAccount result.TransactionID = transaction.ID return nil }) if err != nil { return nil, err } return &result, nil } // GetSavingsPot retrieves a savings pot with progress information func (s *SavingsPotService) GetSavingsPot(userID uint, id uint) (*SavingsPotDetail, error) { var savingsPot models.Account if err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&savingsPot).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrSavingsPotNotFound } return nil, fmt.Errorf("failed to get savings pot: %w", err) } // Verify it's a savings pot if savingsPot.SubAccountType == nil || *savingsPot.SubAccountType != models.SubAccountTypeSavingsPot { return nil, errors.New("account is not a savings pot") } detail := &SavingsPotDetail{ Account: savingsPot, } // Calculate progress if savingsPot.TargetAmount != nil && *savingsPot.TargetAmount > 0 { detail.Progress = (savingsPot.Balance / *savingsPot.TargetAmount) * 100 if detail.Progress > 100 { detail.Progress = 100 } } // Calculate days remaining if savingsPot.TargetDate != nil { now := time.Now() if savingsPot.TargetDate.After(now) { days := int(savingsPot.TargetDate.Sub(now).Hours() / 24) detail.DaysRemaining = &days } else { zero := 0 detail.DaysRemaining = &zero } } return detail, nil } // ListSavingsPots retrieves all savings pots for a main account func (s *SavingsPotService) ListSavingsPots(userID uint, mainAccountID uint) ([]SavingsPotDetail, error) { // Verify main account ownership var mainAccount models.Account if err := s.db.Where("id = ? AND user_id = ?", mainAccountID, userID).First(&mainAccount).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("main account not found") } return nil, fmt.Errorf("failed to get main account: %w", err) } savingsPotType := models.SubAccountTypeSavingsPot var savingsPots []models.Account err := s.db.Where("parent_account_id = ? AND sub_account_type = ? AND user_id = ?", mainAccountID, savingsPotType, userID). Order("sort_order ASC, created_at ASC"). Find(&savingsPots).Error if err != nil { return nil, fmt.Errorf("failed to list savings pots: %w", err) } details := make([]SavingsPotDetail, len(savingsPots)) for i, sp := range savingsPots { details[i] = SavingsPotDetail{Account: sp} // Calculate progress if sp.TargetAmount != nil && *sp.TargetAmount > 0 { details[i].Progress = (sp.Balance / *sp.TargetAmount) * 100 if details[i].Progress > 100 { details[i].Progress = 100 } } // Calculate days remaining if sp.TargetDate != nil { now := time.Now() if sp.TargetDate.After(now) { days := int(sp.TargetDate.Sub(now).Hours() / 24) details[i].DaysRemaining = &days } else { zero := 0 details[i].DaysRemaining = &zero } } } return details, nil }