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,302 @@
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
}