feat: 添加核心财务模型、账户存储库和存钱罐服务

This commit is contained in:
2026-02-01 18:54:05 +08:00
parent 6cf8fd04f4
commit 49fcce531d
3 changed files with 157 additions and 37 deletions

View File

@@ -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 {
tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAccountNotFound
}
return nil, fmt.Errorf("failed to get account: %w", err)
}
sourceAccountID = input.FromAccountID
} else {
sourceAccountID = piggyBank.LinkedAccountID
}
// Check if account has sufficient funds (only for non-credit accounts)
if !account.IsCredit && account.Balance < input.Amount {
tx.Rollback()
return nil, ErrInsufficientAccountFunds
}
// 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)")
}
// Deduct from account
account.Balance -= input.Amount
if err := tx.Save(&account).Error; err != nil {
// 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 source account: %w", err)
}
// Check if account has sufficient funds (only for non-credit accounts)
if !sourceAccount.IsCredit && sourceAccount.Balance < input.Amount {
tx.Rollback()
return nil, ErrInsufficientAccountFunds
}
// 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 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 update account balance: %w", err)
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 {
// 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
}
}
if err := tx.Save(&destinationAccount).Error; err != nil {
tx.Rollback()
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()
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 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),
}
// Add to account
account.Balance += input.Amount
if err := tx.Save(&account).Error; err != nil {
if err := tx.Create(&transaction).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("failed to update account balance: %w", err)
return nil, fmt.Errorf("failed to create transaction record: %w", err)
}
}