feat: 添加核心财务模型、账户存储库和存钱罐服务
This commit is contained in:
@@ -286,11 +286,16 @@ func (Account) TableName() string {
|
|||||||
return "accounts"
|
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
|
// TotalBalance calculates the total balance including sub-accounts
|
||||||
// Feature: financial-core-upgrade
|
// Feature: financial-core-upgrade
|
||||||
// Validates: Requirements 1.3
|
// Validates: Requirements 1.3
|
||||||
func (a *Account) TotalBalance() float64 {
|
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 {
|
for _, sub := range a.SubAccounts {
|
||||||
if sub.SubAccountType != nil && *sub.SubAccountType != SubAccountTypeSavingsPot {
|
if sub.SubAccountType != nil && *sub.SubAccountType != SubAccountTypeSavingsPot {
|
||||||
total += sub.Balance
|
total += sub.Balance
|
||||||
|
|||||||
@@ -146,10 +146,12 @@ func (r *AccountRepository) GetTotalBalance(userID uint) (assets float64, liabil
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, account := range accounts {
|
for _, account := range accounts {
|
||||||
if account.Balance >= 0 {
|
// Calculate true total for the account
|
||||||
assets += account.Balance
|
accountTotal := account.Balance + account.FrozenBalance
|
||||||
|
if accountTotal >= 0 {
|
||||||
|
assets += accountTotal
|
||||||
} else {
|
} 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
|
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
|
// Deposit adds money to a piggy bank
|
||||||
// If fromAccountID is provided, it will deduct from that account
|
// If fromAccountID is provided, it will deduct from that account
|
||||||
func (s *PiggyBankService) Deposit(userID, id uint, input DepositInput) (*models.PiggyBank, error) {
|
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)
|
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 {
|
if input.FromAccountID != nil {
|
||||||
var account models.Account
|
sourceAccountID = input.FromAccountID
|
||||||
if err := tx.Where("user_id = ?", userID).First(&account, *input.FromAccountID).Error; err != nil {
|
} 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()
|
tx.Rollback()
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, ErrAccountNotFound
|
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)
|
// 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()
|
tx.Rollback()
|
||||||
return nil, ErrInsufficientAccountFunds
|
return nil, ErrInsufficientAccountFunds
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduct from account
|
// Deduct from Source Account
|
||||||
account.Balance -= input.Amount
|
sourceAccount.Balance -= input.Amount
|
||||||
if err := tx.Save(&account).Error; err != nil {
|
|
||||||
|
// 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()
|
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)
|
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
|
// Commit transaction
|
||||||
if err := tx.Commit().Error; err != nil {
|
if err := tx.Commit().Error; err != nil {
|
||||||
return nil, fmt.Errorf("failed to commit transaction: %w", err)
|
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)
|
// 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) {
|
func (s *PiggyBankService) Withdraw(userID, id uint, input WithdrawInput) (*models.PiggyBank, error) {
|
||||||
// Validate withdraw amount
|
// Validate withdraw amount
|
||||||
if input.Amount <= 0 {
|
if input.Amount <= 0 {
|
||||||
@@ -356,31 +411,89 @@ func (s *PiggyBankService) Withdraw(userID, id uint, input WithdrawInput) (*mode
|
|||||||
return nil, ErrInsufficientBalance
|
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
|
// Deduct from piggy bank
|
||||||
piggyBank.CurrentAmount -= input.Amount
|
piggyBank.CurrentAmount -= input.Amount
|
||||||
|
|
||||||
// Update piggy bank
|
|
||||||
if err := tx.Save(&piggyBank).Error; err != nil {
|
if err := tx.Save(&piggyBank).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return nil, fmt.Errorf("failed to update piggy bank: %w", err)
|
return nil, fmt.Errorf("failed to update piggy bank: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If toAccountID is provided, add to that account
|
// Add to Destination Account
|
||||||
if input.ToAccountID != nil {
|
destinationAccount.Balance += input.Amount
|
||||||
var account models.Account
|
|
||||||
if err := tx.Where("user_id = ?", userID).First(&account, *input.ToAccountID).Error; err != nil {
|
// If Destination is the Linked Account, we move funds from Frozen (so Frozen decreases)
|
||||||
tx.Rollback()
|
if piggyBank.LinkedAccountID != nil && destinationAccount.ID == *piggyBank.LinkedAccountID {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
destinationAccount.FrozenBalance -= input.Amount
|
||||||
return nil, ErrAccountNotFound
|
// 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
|
if err := tx.Save(&destinationAccount).Error; err != nil {
|
||||||
account.Balance += input.Amount
|
|
||||||
if err := tx.Save(&account).Error; err != nil {
|
|
||||||
tx.Rollback()
|
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