init
This commit is contained in:
302
internal/service/savings_pot_service.go
Normal file
302
internal/service/savings_pot_service.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user