609 lines
21 KiB
Go
609 lines
21 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"accounting-app/internal/models"
|
|
"accounting-app/internal/repository"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Transaction service errors
|
|
var (
|
|
ErrTransactionNotFound = errors.New("transaction not found")
|
|
ErrInvalidTransactionType = errors.New("invalid transaction type")
|
|
ErrMissingRequiredField = errors.New("missing required field")
|
|
ErrInvalidAmount = errors.New("amount must be positive")
|
|
ErrInvalidCurrency = errors.New("invalid currency")
|
|
ErrCategoryNotFoundForTxn = errors.New("category not found")
|
|
ErrAccountNotFoundForTxn = errors.New("account not found")
|
|
ErrToAccountNotFoundForTxn = errors.New("destination account not found for transfer")
|
|
ErrToAccountRequiredForTxn = errors.New("destination account is required for transfer transactions")
|
|
ErrSameAccountTransferForTxn = errors.New("cannot transfer to the same account")
|
|
)
|
|
|
|
// TransactionInput represents the input data for creating or updating a transaction
|
|
type TransactionInput struct {
|
|
UserID uint `json:"user_id"`
|
|
Amount float64 `json:"amount" binding:"required"`
|
|
Type models.TransactionType `json:"type" binding:"required"`
|
|
CategoryID uint `json:"category_id" binding:"required"`
|
|
AccountID uint `json:"account_id" binding:"required"`
|
|
Currency models.Currency `json:"currency" binding:"required"`
|
|
TransactionDate time.Time `json:"transaction_date" binding:"required"`
|
|
Note string `json:"note,omitempty"`
|
|
ImagePath string `json:"image_path,omitempty"`
|
|
ToAccountID *uint `json:"to_account_id,omitempty"`
|
|
TagIDs []uint `json:"tag_ids,omitempty"`
|
|
}
|
|
|
|
// TransactionListInput represents the input for listing transactions
|
|
type TransactionListInput struct {
|
|
UserID *uint `json:"user_id,omitempty"`
|
|
StartDate *time.Time `json:"start_date,omitempty"`
|
|
EndDate *time.Time `json:"end_date,omitempty"`
|
|
CategoryID *uint `json:"category_id,omitempty"`
|
|
AccountID *uint `json:"account_id,omitempty"`
|
|
TagIDs []uint `json:"tag_ids,omitempty"`
|
|
Type *models.TransactionType `json:"type,omitempty"`
|
|
Currency *models.Currency `json:"currency,omitempty"`
|
|
NoteSearch string `json:"note_search,omitempty"`
|
|
SortField string `json:"sort_field,omitempty"`
|
|
SortAsc bool `json:"sort_asc,omitempty"`
|
|
Offset int `json:"offset,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
|
|
// TransactionService handles business logic for transactions
|
|
type TransactionService struct {
|
|
repo *repository.TransactionRepository
|
|
accountRepo *repository.AccountRepository
|
|
categoryRepo *repository.CategoryRepository
|
|
tagRepo *repository.TagRepository
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewTransactionService creates a new TransactionService instance
|
|
func NewTransactionService(
|
|
repo *repository.TransactionRepository,
|
|
accountRepo *repository.AccountRepository,
|
|
categoryRepo *repository.CategoryRepository,
|
|
tagRepo *repository.TagRepository,
|
|
db *gorm.DB,
|
|
) *TransactionService {
|
|
return &TransactionService{
|
|
repo: repo,
|
|
accountRepo: accountRepo,
|
|
categoryRepo: categoryRepo,
|
|
tagRepo: tagRepo,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// ValidateTransactionInput validates the required fields for a transaction
|
|
// Returns an error if any required field is missing or invalid
|
|
// Required fields: Amount, Type, CategoryID, AccountID, Currency, TransactionDate
|
|
func (s *TransactionService) ValidateTransactionInput(input TransactionInput) error {
|
|
// Validate amount (must be positive)
|
|
if input.Amount <= 0 {
|
|
return fmt.Errorf("%w: amount must be greater than 0", ErrInvalidAmount)
|
|
}
|
|
|
|
// Validate transaction type
|
|
if input.Type == "" {
|
|
return fmt.Errorf("%w: type", ErrMissingRequiredField)
|
|
}
|
|
if input.Type != models.TransactionTypeIncome &&
|
|
input.Type != models.TransactionTypeExpense &&
|
|
input.Type != models.TransactionTypeTransfer {
|
|
return ErrInvalidTransactionType
|
|
}
|
|
|
|
// Validate category ID
|
|
if input.CategoryID == 0 {
|
|
return fmt.Errorf("%w: category_id", ErrMissingRequiredField)
|
|
}
|
|
|
|
// Validate account ID
|
|
if input.AccountID == 0 {
|
|
return fmt.Errorf("%w: account_id", ErrMissingRequiredField)
|
|
}
|
|
|
|
// Validate currency
|
|
if input.Currency == "" {
|
|
return fmt.Errorf("%w: currency", ErrMissingRequiredField)
|
|
}
|
|
if !isValidCurrency(input.Currency) {
|
|
return ErrInvalidCurrency
|
|
}
|
|
|
|
// Validate transaction date
|
|
if input.TransactionDate.IsZero() {
|
|
return fmt.Errorf("%w: transaction_date", ErrMissingRequiredField)
|
|
}
|
|
|
|
// Validate transfer-specific fields
|
|
if input.Type == models.TransactionTypeTransfer {
|
|
if input.ToAccountID == nil || *input.ToAccountID == 0 {
|
|
return ErrToAccountRequiredForTxn
|
|
}
|
|
if *input.ToAccountID == input.AccountID {
|
|
return ErrSameAccountTransferForTxn
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isValidCurrency checks if the currency is a supported currency
|
|
func isValidCurrency(currency models.Currency) bool {
|
|
supportedCurrencies := models.SupportedCurrencies()
|
|
for _, c := range supportedCurrencies {
|
|
if c == currency {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CreateTransaction creates a new transaction with business logic validation
|
|
// and automatically updates the account balance
|
|
func (s *TransactionService) CreateTransaction(userID uint, input TransactionInput) (*models.Transaction, error) {
|
|
input.UserID = userID
|
|
// Validate input
|
|
if err := s.ValidateTransactionInput(input); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Execute within a database transaction
|
|
var transaction *models.Transaction
|
|
err := s.db.Transaction(func(tx *gorm.DB) error {
|
|
// Create temporary repositories for this transaction
|
|
txAccountRepo := repository.NewAccountRepository(tx)
|
|
txCategoryRepo := repository.NewCategoryRepository(tx)
|
|
txTransactionRepo := repository.NewTransactionRepository(tx)
|
|
|
|
// Verify category exists
|
|
categoryExists, err := txCategoryRepo.ExistsByID(userID, input.CategoryID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to verify category: %w", err)
|
|
}
|
|
if !categoryExists {
|
|
return ErrCategoryNotFoundForTxn
|
|
}
|
|
|
|
// Verify account exists and get it for balance update
|
|
account, err := txAccountRepo.GetByID(userID, input.AccountID)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrAccountNotFound) {
|
|
return ErrAccountNotFoundForTxn
|
|
}
|
|
return fmt.Errorf("failed to verify account: %w", err)
|
|
}
|
|
|
|
// For transfer transactions, verify destination account
|
|
var toAccount *models.Account
|
|
if input.Type == models.TransactionTypeTransfer {
|
|
toAccount, err = txAccountRepo.GetByID(userID, *input.ToAccountID)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrAccountNotFound) {
|
|
return ErrToAccountNotFoundForTxn
|
|
}
|
|
return fmt.Errorf("failed to verify destination account: %w", err)
|
|
}
|
|
}
|
|
|
|
// Calculate new balance and validate
|
|
newBalance := calculateNewBalance(account.Balance, input.Amount, input.Type, true)
|
|
if !account.IsCredit && newBalance < 0 {
|
|
return ErrInsufficientBalance
|
|
}
|
|
|
|
// Create the transaction model
|
|
transaction = &models.Transaction{
|
|
UserID: input.UserID,
|
|
Amount: input.Amount,
|
|
Type: input.Type,
|
|
CategoryID: input.CategoryID,
|
|
AccountID: input.AccountID,
|
|
Currency: input.Currency,
|
|
TransactionDate: input.TransactionDate,
|
|
Note: input.Note,
|
|
ImagePath: input.ImagePath,
|
|
ToAccountID: input.ToAccountID,
|
|
}
|
|
|
|
// Save transaction with tags
|
|
if len(input.TagIDs) > 0 {
|
|
// Validate tags ownership
|
|
for _, tagID := range input.TagIDs {
|
|
exists, err := s.tagRepo.ExistsByID(userID, tagID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to verify tag %d: %w", tagID, err)
|
|
}
|
|
if !exists {
|
|
return fmt.Errorf("tag %d not found or not owned by user", tagID)
|
|
}
|
|
}
|
|
|
|
if err := txTransactionRepo.CreateWithTags(transaction, input.TagIDs); err != nil {
|
|
return fmt.Errorf("failed to create transaction with tags: %w", err)
|
|
}
|
|
} else {
|
|
if err := txTransactionRepo.Create(transaction); err != nil {
|
|
return fmt.Errorf("failed to create transaction: %w", err)
|
|
}
|
|
}
|
|
|
|
// Update account balance
|
|
if err := txAccountRepo.UpdateBalance(userID, input.AccountID, newBalance); err != nil {
|
|
return fmt.Errorf("failed to update account balance: %w", err)
|
|
}
|
|
|
|
// For transfer transactions, update destination account balance
|
|
if input.Type == models.TransactionTypeTransfer && toAccount != nil {
|
|
newToBalance := toAccount.Balance + input.Amount
|
|
if err := txAccountRepo.UpdateBalance(userID, *input.ToAccountID, newToBalance); err != nil {
|
|
return fmt.Errorf("failed to update destination account balance: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transaction, nil
|
|
}
|
|
|
|
// GetTransaction retrieves a transaction by ID
|
|
// GetTransaction retrieves a transaction by ID and verifies ownership
|
|
func (s *TransactionService) GetTransaction(userID, id uint) (*models.Transaction, error) {
|
|
transaction, err := s.repo.GetByIDWithRelations(userID, id)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrTransactionNotFound) {
|
|
return nil, ErrTransactionNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to get transaction: %w", err)
|
|
}
|
|
if transaction.UserID != userID {
|
|
return nil, ErrTransactionNotFound
|
|
}
|
|
return transaction, nil
|
|
}
|
|
|
|
// UpdateTransaction updates an existing transaction and adjusts account balances, verifying ownership
|
|
func (s *TransactionService) UpdateTransaction(userID, id uint, input TransactionInput) (*models.Transaction, error) {
|
|
// Validate input
|
|
if err := s.ValidateTransactionInput(input); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var transaction *models.Transaction
|
|
err := s.db.Transaction(func(tx *gorm.DB) error {
|
|
// Create temporary repositories for this transaction
|
|
txAccountRepo := repository.NewAccountRepository(tx)
|
|
txCategoryRepo := repository.NewCategoryRepository(tx)
|
|
txTransactionRepo := repository.NewTransactionRepository(tx)
|
|
|
|
// Get existing transaction
|
|
existingTxn, err := txTransactionRepo.GetByID(userID, id)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrTransactionNotFound) {
|
|
return ErrTransactionNotFound
|
|
}
|
|
return fmt.Errorf("failed to get existing transaction: %w", err)
|
|
}
|
|
if existingTxn.UserID != userID {
|
|
return ErrTransactionNotFound
|
|
}
|
|
|
|
// Verify category exists
|
|
categoryExists, err := txCategoryRepo.ExistsByID(userID, input.CategoryID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to verify category: %w", err)
|
|
}
|
|
if !categoryExists {
|
|
return ErrCategoryNotFoundForTxn
|
|
}
|
|
|
|
// Get old account for balance reversal
|
|
oldAccount, err := txAccountRepo.GetByID(userID, existingTxn.AccountID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get old account: %w", err)
|
|
}
|
|
|
|
// Get new account
|
|
newAccount, err := txAccountRepo.GetByID(userID, input.AccountID)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrAccountNotFound) {
|
|
return ErrAccountNotFoundForTxn
|
|
}
|
|
return fmt.Errorf("failed to get new account: %w", err)
|
|
}
|
|
|
|
// Handle old transfer destination account
|
|
var oldToAccount *models.Account
|
|
if existingTxn.Type == models.TransactionTypeTransfer && existingTxn.ToAccountID != nil {
|
|
oldToAccount, err = txAccountRepo.GetByID(userID, *existingTxn.ToAccountID)
|
|
if err != nil && !errors.Is(err, repository.ErrAccountNotFound) {
|
|
return fmt.Errorf("failed to get old destination account: %w", err)
|
|
}
|
|
}
|
|
|
|
// Handle new transfer destination account
|
|
var newToAccount *models.Account
|
|
if input.Type == models.TransactionTypeTransfer {
|
|
newToAccount, err = txAccountRepo.GetByID(userID, *input.ToAccountID)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrAccountNotFound) {
|
|
return ErrToAccountNotFoundForTxn
|
|
}
|
|
return fmt.Errorf("failed to get new destination account: %w", err)
|
|
}
|
|
}
|
|
|
|
// Step 1: Reverse the old transaction's effect on balances
|
|
oldReversedBalance := calculateNewBalance(oldAccount.Balance, existingTxn.Amount, existingTxn.Type, false)
|
|
if err := txAccountRepo.UpdateBalance(userID, existingTxn.AccountID, oldReversedBalance); err != nil {
|
|
return fmt.Errorf("failed to reverse old account balance: %w", err)
|
|
}
|
|
|
|
// Reverse old transfer destination if applicable
|
|
if oldToAccount != nil {
|
|
oldToReversedBalance := oldToAccount.Balance - existingTxn.Amount
|
|
if err := txAccountRepo.UpdateBalance(userID, *existingTxn.ToAccountID, oldToReversedBalance); err != nil {
|
|
return fmt.Errorf("failed to reverse old destination account balance: %w", err)
|
|
}
|
|
}
|
|
|
|
// Step 2: Apply the new transaction's effect on balances
|
|
// Re-fetch account if it's the same as old account (balance was just updated)
|
|
if input.AccountID == existingTxn.AccountID {
|
|
newAccount, err = txAccountRepo.GetByID(userID, input.AccountID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to re-fetch account: %w", err)
|
|
}
|
|
}
|
|
|
|
newBalance := calculateNewBalance(newAccount.Balance, input.Amount, input.Type, true)
|
|
if !newAccount.IsCredit && newBalance < 0 {
|
|
return ErrInsufficientBalance
|
|
}
|
|
|
|
if err := txAccountRepo.UpdateBalance(userID, input.AccountID, newBalance); err != nil {
|
|
return fmt.Errorf("failed to update new account balance: %w", err)
|
|
}
|
|
|
|
// Apply new transfer destination if applicable
|
|
if newToAccount != nil {
|
|
// Re-fetch if it's the same as old destination (balance was just updated)
|
|
if existingTxn.ToAccountID != nil && *input.ToAccountID == *existingTxn.ToAccountID {
|
|
newToAccount, err = txAccountRepo.GetByID(userID, *input.ToAccountID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to re-fetch destination account: %w", err)
|
|
}
|
|
}
|
|
newToBalance := newToAccount.Balance + input.Amount
|
|
if err := txAccountRepo.UpdateBalance(userID, *input.ToAccountID, newToBalance); err != nil {
|
|
return fmt.Errorf("failed to update new destination account balance: %w", err)
|
|
}
|
|
}
|
|
|
|
// Step 3: Update the transaction record
|
|
transaction = existingTxn
|
|
transaction.Amount = input.Amount
|
|
transaction.Type = input.Type
|
|
transaction.CategoryID = input.CategoryID
|
|
transaction.AccountID = input.AccountID
|
|
transaction.Currency = input.Currency
|
|
transaction.TransactionDate = input.TransactionDate
|
|
transaction.Note = input.Note
|
|
transaction.ImagePath = input.ImagePath
|
|
transaction.ToAccountID = input.ToAccountID
|
|
|
|
if err := txTransactionRepo.UpdateWithTags(transaction, input.TagIDs); err != nil {
|
|
return fmt.Errorf("failed to update transaction: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transaction, nil
|
|
}
|
|
|
|
// DeleteTransaction deletes a transaction and reverses its effect on account balance, verifying ownership
|
|
func (s *TransactionService) DeleteTransaction(userID, id uint) error {
|
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
|
// Create temporary repositories for this transaction
|
|
txAccountRepo := repository.NewAccountRepository(tx)
|
|
txTransactionRepo := repository.NewTransactionRepository(tx)
|
|
|
|
// Get existing transaction
|
|
existingTxn, err := txTransactionRepo.GetByID(userID, id)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrTransactionNotFound) {
|
|
return ErrTransactionNotFound
|
|
}
|
|
return fmt.Errorf("failed to get transaction: %w", err)
|
|
}
|
|
if existingTxn.UserID != userID {
|
|
return ErrTransactionNotFound
|
|
}
|
|
|
|
// Get account for balance reversal
|
|
account, err := txAccountRepo.GetByID(userID, existingTxn.AccountID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get account: %w", err)
|
|
}
|
|
|
|
// Reverse the transaction's effect on balance
|
|
reversedBalance := calculateNewBalance(account.Balance, existingTxn.Amount, existingTxn.Type, false)
|
|
if err := txAccountRepo.UpdateBalance(userID, existingTxn.AccountID, reversedBalance); err != nil {
|
|
return fmt.Errorf("failed to reverse account balance: %w", err)
|
|
}
|
|
|
|
// For transfer transactions, reverse destination account balance
|
|
if existingTxn.Type == models.TransactionTypeTransfer && existingTxn.ToAccountID != nil {
|
|
toAccount, err := txAccountRepo.GetByID(userID, *existingTxn.ToAccountID)
|
|
if err != nil && !errors.Is(err, repository.ErrAccountNotFound) {
|
|
return fmt.Errorf("failed to get destination account: %w", err)
|
|
}
|
|
if toAccount != nil {
|
|
reversedToBalance := toAccount.Balance - existingTxn.Amount
|
|
if err := txAccountRepo.UpdateBalance(userID, *existingTxn.ToAccountID, reversedToBalance); err != nil {
|
|
return fmt.Errorf("failed to reverse destination account balance: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete the transaction
|
|
if err := txTransactionRepo.Delete(userID, id); err != nil {
|
|
return fmt.Errorf("failed to delete transaction: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// ListTransactions retrieves transactions with filtering and pagination
|
|
func (s *TransactionService) ListTransactions(userID uint, input TransactionListInput) (*repository.TransactionListResult, error) {
|
|
// Set default limit if not provided
|
|
limit := input.Limit
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
|
|
options := repository.TransactionListOptions{
|
|
Filter: repository.TransactionFilter{
|
|
UserID: &userID,
|
|
StartDate: input.StartDate,
|
|
EndDate: input.EndDate,
|
|
CategoryID: input.CategoryID,
|
|
AccountID: input.AccountID,
|
|
TagIDs: input.TagIDs,
|
|
Type: input.Type,
|
|
Currency: input.Currency,
|
|
NoteSearch: input.NoteSearch,
|
|
},
|
|
Sort: repository.TransactionSort{
|
|
Field: input.SortField,
|
|
Ascending: input.SortAsc,
|
|
},
|
|
Offset: input.Offset,
|
|
Limit: limit,
|
|
}
|
|
|
|
result, err := s.repo.List(userID, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list transactions: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetTransactionsByAccount retrieves all transactions for a specific account
|
|
func (s *TransactionService) GetTransactionsByAccount(userID uint, accountID uint) ([]models.Transaction, error) {
|
|
// Verify account exists
|
|
_, err := s.accountRepo.GetByID(userID, accountID)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrAccountNotFound) {
|
|
return nil, ErrAccountNotFoundForTxn
|
|
}
|
|
return nil, fmt.Errorf("failed to verify account: %w", err)
|
|
}
|
|
|
|
transactions, err := s.repo.GetByAccountID(userID, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get transactions by account: %w", err)
|
|
}
|
|
return transactions, nil
|
|
}
|
|
|
|
// GetTransactionsByCategory retrieves all transactions for a specific category
|
|
func (s *TransactionService) GetTransactionsByCategory(userID uint, categoryID uint) ([]models.Transaction, error) {
|
|
// Verify category exists
|
|
exists, err := s.categoryRepo.ExistsByID(userID, categoryID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to verify category: %w", err)
|
|
}
|
|
if !exists {
|
|
return nil, ErrCategoryNotFoundForTxn
|
|
}
|
|
|
|
transactions, err := s.repo.GetByCategoryID(userID, categoryID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get transactions by category: %w", err)
|
|
}
|
|
return transactions, nil
|
|
}
|
|
|
|
// GetTransactionsByDateRange retrieves all transactions within a date range
|
|
func (s *TransactionService) GetTransactionsByDateRange(userID uint, startDate, endDate time.Time) ([]models.Transaction, error) {
|
|
transactions, err := s.repo.GetByDateRange(userID, startDate, endDate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get transactions by date range: %w", err)
|
|
}
|
|
return transactions, nil
|
|
}
|
|
|
|
// GetRecentTransactions retrieves the most recent transactions
|
|
func (s *TransactionService) GetRecentTransactions(userID uint, limit int) ([]models.Transaction, error) {
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
transactions, err := s.repo.GetRecentTransactions(userID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get recent transactions: %w", err)
|
|
}
|
|
return transactions, nil
|
|
}
|
|
|
|
// GetRelatedTransactions retrieves all related transactions for a given transaction ID
|
|
// Returns the relationship between original expense/refund income/reimbursement income
|
|
// Feature: accounting-feature-upgrade
|
|
// Validates: Requirements 8.21, 8.22
|
|
func (s *TransactionService) GetRelatedTransactions(userID uint, id uint) ([]models.Transaction, error) {
|
|
relatedTransactions, err := s.repo.GetRelatedTransactions(userID, id)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrTransactionNotFound) {
|
|
return nil, ErrTransactionNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to get related transactions: %w", err)
|
|
}
|
|
return relatedTransactions, nil
|
|
}
|
|
|
|
// calculateNewBalance calculates the new balance after a transaction
|
|
// isApply: true for applying a transaction, false for reversing it
|
|
func calculateNewBalance(currentBalance, amount float64, txnType models.TransactionType, isApply bool) float64 {
|
|
var change float64
|
|
|
|
switch txnType {
|
|
case models.TransactionTypeIncome:
|
|
change = amount
|
|
case models.TransactionTypeExpense:
|
|
change = -amount
|
|
case models.TransactionTypeTransfer:
|
|
// For the source account, transfer is like an expense
|
|
change = -amount
|
|
default:
|
|
return currentBalance
|
|
}
|
|
|
|
if isApply {
|
|
return currentBalance + change
|
|
}
|
|
// Reverse: subtract the change (or add the negative)
|
|
return currentBalance - change
|
|
}
|