feat: 添加核心财务模型、账户存储库和存钱罐服务
This commit is contained in:
@@ -286,11 +286,16 @@ func (Account) TableName() string {
|
||||
return "accounts"
|
||||
}
|
||||
|
||||
// TotalBalance calculates the total balance including sub-accounts
|
||||
// Feature: financial-core-upgrade
|
||||
// Validates: Requirements 1.3
|
||||
// TotalBalance calculates the total balance including sub-accounts
|
||||
// Feature: financial-core-upgrade
|
||||
// Validates: Requirements 1.3
|
||||
func (a *Account) TotalBalance() float64 {
|
||||
total := a.AvailableBalance + a.FrozenBalance
|
||||
// Balance is treated as Available Balance (liquid funds)
|
||||
// FrozenBalance represents funds locked in savings pots/piggy banks
|
||||
total := a.Balance + a.FrozenBalance
|
||||
for _, sub := range a.SubAccounts {
|
||||
if sub.SubAccountType != nil && *sub.SubAccountType != SubAccountTypeSavingsPot {
|
||||
total += sub.Balance
|
||||
|
||||
@@ -146,10 +146,12 @@ func (r *AccountRepository) GetTotalBalance(userID uint) (assets float64, liabil
|
||||
}
|
||||
|
||||
for _, account := range accounts {
|
||||
if account.Balance >= 0 {
|
||||
assets += account.Balance
|
||||
// Calculate true total for the account
|
||||
accountTotal := account.Balance + account.FrozenBalance
|
||||
if accountTotal >= 0 {
|
||||
assets += accountTotal
|
||||
} else {
|
||||
liabilities += -account.Balance // Convert to positive for liabilities
|
||||
liabilities += -accountTotal // Convert to positive for liabilities
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -256,6 +256,8 @@ func (s *PiggyBankService) DeletePiggyBank(userID, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deposit adds money to a piggy bank
|
||||
// If fromAccountID is provided, it will deduct from that account
|
||||
// Deposit adds money to a piggy bank
|
||||
// If fromAccountID is provided, it will deduct from that account
|
||||
func (s *PiggyBankService) Deposit(userID, id uint, input DepositInput) (*models.PiggyBank, error) {
|
||||
@@ -282,28 +284,60 @@ func (s *PiggyBankService) Deposit(userID, id uint, input DepositInput) (*models
|
||||
return nil, fmt.Errorf("failed to get piggy bank: %w", err)
|
||||
}
|
||||
|
||||
// If fromAccountID is provided, deduct from that account
|
||||
// Determine source account (Input > Linked)
|
||||
var sourceAccountID *uint
|
||||
if input.FromAccountID != nil {
|
||||
var account models.Account
|
||||
if err := tx.Where("user_id = ?", userID).First(&account, *input.FromAccountID).Error; err != nil {
|
||||
sourceAccountID = input.FromAccountID
|
||||
} else {
|
||||
sourceAccountID = piggyBank.LinkedAccountID
|
||||
}
|
||||
|
||||
// Verify we have a source account for deduction
|
||||
if sourceAccountID == nil {
|
||||
tx.Rollback()
|
||||
return nil, errors.New("cannot deposit without a source account (linked or specified)")
|
||||
}
|
||||
|
||||
// Get Source Account
|
||||
var sourceAccount models.Account
|
||||
if err := tx.Where("user_id = ?", userID).First(&sourceAccount, *sourceAccountID).Error; err != nil {
|
||||
tx.Rollback()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAccountNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get account: %w", err)
|
||||
return nil, fmt.Errorf("failed to get source account: %w", err)
|
||||
}
|
||||
|
||||
// Check if account has sufficient funds (only for non-credit accounts)
|
||||
if !account.IsCredit && account.Balance < input.Amount {
|
||||
if !sourceAccount.IsCredit && sourceAccount.Balance < input.Amount {
|
||||
tx.Rollback()
|
||||
return nil, ErrInsufficientAccountFunds
|
||||
}
|
||||
|
||||
// Deduct from account
|
||||
account.Balance -= input.Amount
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
// Deduct from Source Account
|
||||
sourceAccount.Balance -= input.Amount
|
||||
|
||||
// If Source is the Linked Account, we move funds to Frozen
|
||||
if piggyBank.LinkedAccountID != nil && sourceAccount.ID == *piggyBank.LinkedAccountID {
|
||||
sourceAccount.FrozenBalance += input.Amount
|
||||
}
|
||||
|
||||
if err := tx.Save(&sourceAccount).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("failed to update account balance: %w", err)
|
||||
return nil, fmt.Errorf("failed to update source account balance: %w", err)
|
||||
}
|
||||
|
||||
// If Source is NOT the Linked Account (and Linked exists), we must update Linked Account's Frozen Balance
|
||||
if piggyBank.LinkedAccountID != nil && sourceAccount.ID != *piggyBank.LinkedAccountID {
|
||||
var linkedAccount models.Account
|
||||
if err := tx.Where("user_id = ?", userID).First(&linkedAccount, *piggyBank.LinkedAccountID).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("failed to get linked account: %w", err)
|
||||
}
|
||||
linkedAccount.FrozenBalance += input.Amount
|
||||
if err := tx.Save(&linkedAccount).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("failed to update linked account frozen balance: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +350,27 @@ func (s *PiggyBankService) Deposit(userID, id uint, input DepositInput) (*models
|
||||
return nil, fmt.Errorf("failed to update piggy bank: %w", err)
|
||||
}
|
||||
|
||||
// Create Transaction Record
|
||||
var category models.Category
|
||||
if err := tx.Where("user_id = ?", userID).Order("id asc").First(&category).Error; err == nil {
|
||||
subTypeDeposit := models.TransactionSubTypeSavingsDeposit
|
||||
transaction := models.Transaction{
|
||||
UserID: userID,
|
||||
Amount: input.Amount,
|
||||
Type: models.TransactionTypeTransfer,
|
||||
SubType: &subTypeDeposit,
|
||||
CategoryID: category.ID,
|
||||
AccountID: sourceAccount.ID,
|
||||
TransactionDate: time.Now(),
|
||||
Note: fmt.Sprintf("Deposit to piggy bank: %s", piggyBank.Name),
|
||||
}
|
||||
|
||||
if err := tx.Create(&transaction).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("failed to create transaction record: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to commit transaction: %w", err)
|
||||
@@ -325,7 +380,7 @@ func (s *PiggyBankService) Deposit(userID, id uint, input DepositInput) (*models
|
||||
}
|
||||
|
||||
// Withdraw removes money from a piggy bank (breaking the piggy bank)
|
||||
// If toAccountID is provided, it will add to that account
|
||||
// If toAccountID is provided, it will add to that account. Otherwise defaults to Linked Account.
|
||||
func (s *PiggyBankService) Withdraw(userID, id uint, input WithdrawInput) (*models.PiggyBank, error) {
|
||||
// Validate withdraw amount
|
||||
if input.Amount <= 0 {
|
||||
@@ -356,31 +411,89 @@ func (s *PiggyBankService) Withdraw(userID, id uint, input WithdrawInput) (*mode
|
||||
return nil, ErrInsufficientBalance
|
||||
}
|
||||
|
||||
// Determine destination account (Input > Linked)
|
||||
var destinationAccountID *uint
|
||||
if input.ToAccountID != nil {
|
||||
destinationAccountID = input.ToAccountID
|
||||
} else {
|
||||
destinationAccountID = piggyBank.LinkedAccountID
|
||||
}
|
||||
|
||||
// Verify we have a destination account
|
||||
if destinationAccountID == nil {
|
||||
tx.Rollback()
|
||||
return nil, errors.New("cannot withdraw without a destination account (linked or specified)")
|
||||
}
|
||||
|
||||
// Get Destination Account
|
||||
var destinationAccount models.Account
|
||||
if err := tx.Where("user_id = ?", userID).First(&destinationAccount, *destinationAccountID).Error; err != nil {
|
||||
tx.Rollback()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAccountNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get destination account: %w", err)
|
||||
}
|
||||
|
||||
// Deduct from piggy bank
|
||||
piggyBank.CurrentAmount -= input.Amount
|
||||
|
||||
// Update piggy bank
|
||||
if err := tx.Save(&piggyBank).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("failed to update piggy bank: %w", err)
|
||||
}
|
||||
|
||||
// If toAccountID is provided, add to that account
|
||||
if input.ToAccountID != nil {
|
||||
var account models.Account
|
||||
if err := tx.Where("user_id = ?", userID).First(&account, *input.ToAccountID).Error; err != nil {
|
||||
tx.Rollback()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAccountNotFound
|
||||
// Add to Destination Account
|
||||
destinationAccount.Balance += input.Amount
|
||||
|
||||
// If Destination is the Linked Account, we move funds from Frozen (so Frozen decreases)
|
||||
if piggyBank.LinkedAccountID != nil && destinationAccount.ID == *piggyBank.LinkedAccountID {
|
||||
destinationAccount.FrozenBalance -= input.Amount
|
||||
// Safety check for negative frozen balance
|
||||
if destinationAccount.FrozenBalance < 0 {
|
||||
destinationAccount.FrozenBalance = 0
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get account: %w", err)
|
||||
}
|
||||
|
||||
// Add to account
|
||||
account.Balance += input.Amount
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
if err := tx.Save(&destinationAccount).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("failed to update account balance: %w", err)
|
||||
return nil, fmt.Errorf("failed to update destination account balance: %w", err)
|
||||
}
|
||||
|
||||
// If Destination is NOT the Linked Account (and Linked exists), we must update Linked Account's Frozen Balance
|
||||
if piggyBank.LinkedAccountID != nil && destinationAccount.ID != *piggyBank.LinkedAccountID {
|
||||
var linkedAccount models.Account
|
||||
if err := tx.Where("user_id = ?", userID).First(&linkedAccount, *piggyBank.LinkedAccountID).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("failed to get linked account: %w", err)
|
||||
}
|
||||
linkedAccount.FrozenBalance -= input.Amount
|
||||
if linkedAccount.FrozenBalance < 0 {
|
||||
linkedAccount.FrozenBalance = 0
|
||||
}
|
||||
if err := tx.Save(&linkedAccount).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("failed to update linked account frozen balance: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create Transaction Record
|
||||
var category models.Category
|
||||
if err := tx.Where("user_id = ?", userID).Order("id asc").First(&category).Error; err == nil {
|
||||
subTypeWithdraw := models.TransactionSubTypeSavingsWithdraw
|
||||
transaction := models.Transaction{
|
||||
UserID: userID,
|
||||
Amount: input.Amount,
|
||||
Type: models.TransactionTypeTransfer,
|
||||
SubType: &subTypeWithdraw,
|
||||
CategoryID: category.ID,
|
||||
AccountID: destinationAccount.ID,
|
||||
TransactionDate: time.Now(),
|
||||
Note: fmt.Sprintf("Withdraw from piggy bank: %s", piggyBank.Name),
|
||||
}
|
||||
|
||||
if err := tx.Create(&transaction).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("failed to create transaction record: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user