package service import ( "errors" "fmt" "time" "accounting-app/internal/models" "accounting-app/internal/repository" "gorm.io/gorm" "gorm.io/gorm/clause" ) // Refund service errors var ( ErrInvalidRefundAmount = errors.New("refund amount must be greater than 0 and not exceed original amount") ErrAlreadyRefunded = errors.New("transaction already refunded") ErrRefundCategoryNotFound = errors.New("refund system category not found") ) // RefundService handles business logic for refund operations // Feature: accounting-feature-upgrade // Validates: Requirements 8.10-8.18 type RefundService struct { db *gorm.DB transactionRepo *repository.TransactionRepository accountRepo *repository.AccountRepository } // NewRefundService creates a new RefundService instance func NewRefundService( db *gorm.DB, transactionRepo *repository.TransactionRepository, accountRepo *repository.AccountRepository, ) *RefundService { return &RefundService{ db: db, transactionRepo: transactionRepo, accountRepo: accountRepo, } } // ProcessRefund processes a refund on an expense transaction // This automatically creates a refund income record and updates the original transaction // Feature: accounting-feature-upgrade // Validates: Requirements 8.10-8.18, 8.28 func (s *RefundService) ProcessRefund(userID uint, transactionID uint, refundAmount float64) (*models.Transaction, error) { var refundIncome *models.Transaction err := s.db.Transaction(func(tx *gorm.DB) error { txTransactionRepo := repository.NewTransactionRepository(tx) txAccountRepo := repository.NewAccountRepository(tx) // Get the original transaction with lock originalTxn, err := txTransactionRepo.GetByID(userID, transactionID) if err != nil { if errors.Is(err, repository.ErrTransactionNotFound) { return ErrTransactionNotFound } return fmt.Errorf("failed to get transaction: %w", err) } // Lock the transaction for update if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). First(&models.Transaction{}, transactionID).Error; err != nil { return fmt.Errorf("failed to lock transaction: %w", err) } // Validate: must be an expense transaction if originalTxn.Type != models.TransactionTypeExpense { return ErrNotExpenseTransaction } // Validate: cannot refund if already refunded if originalTxn.RefundStatus != "none" { return ErrAlreadyRefunded } // Validate: refund amount must be positive and not exceed original amount if refundAmount <= 0 || refundAmount > originalTxn.Amount { return ErrInvalidRefundAmount } // Get the refund system category var refundCategory models.SystemCategory if err := tx.Where("code = ?", "refund").First(&refundCategory).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrRefundCategoryNotFound } return fmt.Errorf("failed to get refund category: %w", err) } // Get the account to update balance account, err := txAccountRepo.GetByID(userID, originalTxn.AccountID) if err != nil { return fmt.Errorf("failed to get account: %w", err) } // Create the refund income record refundIncome = &models.Transaction{ UserID: userID, Type: models.TransactionTypeIncome, Amount: refundAmount, CategoryID: uint(refundCategory.ID), AccountID: originalTxn.AccountID, Currency: originalTxn.Currency, TransactionDate: time.Now(), Note: fmt.Sprintf("閫€娆?- %s", originalTxn.Note), IncomeType: "refund", OriginalTransactionID: &transactionID, LedgerID: originalTxn.LedgerID, // Same ledger as original transaction (Requirement 8.28) } if err := txTransactionRepo.Create(refundIncome); err != nil { return fmt.Errorf("failed to create refund income: %w", err) } // Determine refund status: partial or full refundStatus := "partial" if refundAmount == originalTxn.Amount { refundStatus = "full" } // Update the original transaction status updates := map[string]interface{}{ "refund_status": refundStatus, "refund_amount": refundAmount, "refund_income_id": refundIncome.ID, } if err := tx.Model(&models.Transaction{}). Where("id = ?", transactionID). Updates(updates).Error; err != nil { return fmt.Errorf("failed to update transaction status: %w", err) } // Update account balance (add the refund income) newBalance := account.Balance + refundAmount if err := txAccountRepo.UpdateBalance(userID, originalTxn.AccountID, newBalance); err != nil { return fmt.Errorf("failed to update account balance: %w", err) } return nil }) if err != nil { return nil, err } return refundIncome, nil }