Merge branch 'fix/calculation-accuracy' into main
All checks were successful
Deploy / build-and-deploy (push) Successful in 2m37s
All checks were successful
Deploy / build-and-deploy (push) Successful in 2m37s
Combines the decimal migration (int cents → decimal.Decimal via shopspring/decimal) with main's handler refactor (split space.go into domain handlers, WithTx/Paginate helpers, recurring deposit removal). - Repository layer: WithTx pattern + decimal column names/types - Handler layer: decimal arithmetic (.Sub/.Add) instead of int operators - Models: deprecated amount_cents fields kept for SELECT * compatibility - INSERT statements: old columns set to literal 0 for NOT NULL constraints Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
89c5d76e5e
46 changed files with 661 additions and 539 deletions
|
|
@ -7,12 +7,13 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type CreateBudgetDTO struct {
|
||||
SpaceID string
|
||||
TagIDs []string
|
||||
Amount int
|
||||
Amount decimal.Decimal
|
||||
Period model.BudgetPeriod
|
||||
StartDate time.Time
|
||||
EndDate *time.Time
|
||||
|
|
@ -22,7 +23,7 @@ type CreateBudgetDTO struct {
|
|||
type UpdateBudgetDTO struct {
|
||||
ID string
|
||||
TagIDs []string
|
||||
Amount int
|
||||
Amount decimal.Decimal
|
||||
Period model.BudgetPeriod
|
||||
StartDate time.Time
|
||||
EndDate *time.Time
|
||||
|
|
@ -37,7 +38,7 @@ func NewBudgetService(budgetRepo repository.BudgetRepository) *BudgetService {
|
|||
}
|
||||
|
||||
func (s *BudgetService) CreateBudget(dto CreateBudgetDTO) (*model.Budget, error) {
|
||||
if dto.Amount <= 0 {
|
||||
if dto.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("budget amount must be positive")
|
||||
}
|
||||
|
||||
|
|
@ -47,16 +48,16 @@ func (s *BudgetService) CreateBudget(dto CreateBudgetDTO) (*model.Budget, error)
|
|||
|
||||
now := time.Now()
|
||||
budget := &model.Budget{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: dto.SpaceID,
|
||||
AmountCents: dto.Amount,
|
||||
Period: dto.Period,
|
||||
StartDate: dto.StartDate,
|
||||
EndDate: dto.EndDate,
|
||||
IsActive: true,
|
||||
CreatedBy: dto.CreatedBy,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: dto.SpaceID,
|
||||
Amount: dto.Amount,
|
||||
Period: dto.Period,
|
||||
StartDate: dto.StartDate,
|
||||
EndDate: dto.EndDate,
|
||||
IsActive: true,
|
||||
CreatedBy: dto.CreatedBy,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := s.budgetRepo.Create(budget, dto.TagIDs); err != nil {
|
||||
|
|
@ -99,12 +100,12 @@ func (s *BudgetService) GetBudgetsWithSpent(spaceID string) ([]*model.BudgetWith
|
|||
start, end := GetCurrentPeriodBounds(b.Period, time.Now())
|
||||
spent, err := s.budgetRepo.GetSpentForBudget(spaceID, tagIDs, start, end)
|
||||
if err != nil {
|
||||
spent = 0
|
||||
spent = decimal.Zero
|
||||
}
|
||||
|
||||
var percentage float64
|
||||
if b.AmountCents > 0 {
|
||||
percentage = float64(spent) / float64(b.AmountCents) * 100
|
||||
if b.Amount.GreaterThan(decimal.Zero) {
|
||||
percentage, _ = spent.Div(b.Amount).Mul(decimal.NewFromInt(100)).Float64()
|
||||
}
|
||||
|
||||
var status model.BudgetStatus
|
||||
|
|
@ -120,7 +121,7 @@ func (s *BudgetService) GetBudgetsWithSpent(spaceID string) ([]*model.BudgetWith
|
|||
bws := &model.BudgetWithSpent{
|
||||
Budget: *b,
|
||||
Tags: tags,
|
||||
SpentCents: spent,
|
||||
Spent: spent,
|
||||
Percentage: percentage,
|
||||
Status: status,
|
||||
}
|
||||
|
|
@ -131,7 +132,7 @@ func (s *BudgetService) GetBudgetsWithSpent(spaceID string) ([]*model.BudgetWith
|
|||
}
|
||||
|
||||
func (s *BudgetService) UpdateBudget(dto UpdateBudgetDTO) (*model.Budget, error) {
|
||||
if dto.Amount <= 0 {
|
||||
if dto.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("budget amount must be positive")
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +145,7 @@ func (s *BudgetService) UpdateBudget(dto UpdateBudgetDTO) (*model.Budget, error)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
existing.AmountCents = dto.Amount
|
||||
existing.Amount = dto.Amount
|
||||
existing.Period = dto.Period
|
||||
existing.StartDate = dto.StartDate
|
||||
existing.EndDate = dto.EndDate
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type CreateExpenseDTO struct {
|
||||
SpaceID string
|
||||
UserID string
|
||||
Description string
|
||||
Amount int
|
||||
Amount decimal.Decimal
|
||||
Type model.ExpenseType
|
||||
Date time.Time
|
||||
TagIDs []string
|
||||
|
|
@ -25,7 +26,7 @@ type UpdateExpenseDTO struct {
|
|||
ID string
|
||||
SpaceID string
|
||||
Description string
|
||||
Amount int
|
||||
Amount decimal.Decimal
|
||||
Type model.ExpenseType
|
||||
Date time.Time
|
||||
TagIDs []string
|
||||
|
|
@ -48,7 +49,7 @@ func (s *ExpenseService) CreateExpense(dto CreateExpenseDTO) (*model.Expense, er
|
|||
if dto.Description == "" {
|
||||
return nil, fmt.Errorf("expense description cannot be empty")
|
||||
}
|
||||
if dto.Amount <= 0 {
|
||||
if dto.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ func (s *ExpenseService) CreateExpense(dto CreateExpenseDTO) (*model.Expense, er
|
|||
SpaceID: dto.SpaceID,
|
||||
CreatedBy: dto.UserID,
|
||||
Description: dto.Description,
|
||||
AmountCents: dto.Amount,
|
||||
Amount: dto.Amount,
|
||||
Type: dto.Type,
|
||||
Date: dto.Date,
|
||||
PaymentMethodID: dto.PaymentMethodID,
|
||||
|
|
@ -78,18 +79,18 @@ func (s *ExpenseService) GetExpensesForSpace(spaceID string) ([]*model.Expense,
|
|||
return s.expenseRepo.GetBySpaceID(spaceID)
|
||||
}
|
||||
|
||||
func (s *ExpenseService) GetBalanceForSpace(spaceID string) (int, error) {
|
||||
func (s *ExpenseService) GetBalanceForSpace(spaceID string) (decimal.Decimal, error) {
|
||||
expenses, err := s.expenseRepo.GetBySpaceID(spaceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return decimal.Zero, err
|
||||
}
|
||||
|
||||
var balance int
|
||||
balance := decimal.Zero
|
||||
for _, expense := range expenses {
|
||||
if expense.Type == model.ExpenseTypeExpense {
|
||||
balance -= expense.AmountCents
|
||||
balance = balance.Sub(expense.Amount)
|
||||
} else if expense.Type == model.ExpenseTypeTopup {
|
||||
balance += expense.AmountCents
|
||||
balance = balance.Add(expense.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +213,7 @@ func (s *ExpenseService) UpdateExpense(dto UpdateExpenseDTO) (*model.Expense, er
|
|||
if dto.Description == "" {
|
||||
return nil, fmt.Errorf("expense description cannot be empty")
|
||||
}
|
||||
if dto.Amount <= 0 {
|
||||
if dto.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +223,7 @@ func (s *ExpenseService) UpdateExpense(dto UpdateExpenseDTO) (*model.Expense, er
|
|||
}
|
||||
|
||||
existing.Description = dto.Description
|
||||
existing.AmountCents = dto.Amount
|
||||
existing.Amount = dto.Amount
|
||||
existing.Type = dto.Type
|
||||
existing.Date = dto.Date
|
||||
existing.PaymentMethodID = dto.PaymentMethodID
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -24,7 +25,7 @@ func TestExpenseService_CreateExpense(t *testing.T) {
|
|||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Lunch",
|
||||
Amount: 1500,
|
||||
Amount: decimal.RequireFromString("15.00"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
TagIDs: []string{tag.ID},
|
||||
|
|
@ -32,7 +33,7 @@ func TestExpenseService_CreateExpense(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, expense.ID)
|
||||
assert.Equal(t, "Lunch", expense.Description)
|
||||
assert.Equal(t, 1500, expense.AmountCents)
|
||||
assert.True(t, decimal.RequireFromString("15.00").Equal(expense.Amount))
|
||||
assert.Equal(t, model.ExpenseTypeExpense, expense.Type)
|
||||
})
|
||||
}
|
||||
|
|
@ -46,7 +47,7 @@ func TestExpenseService_CreateExpense_EmptyDescription(t *testing.T) {
|
|||
SpaceID: "some-space",
|
||||
UserID: "some-user",
|
||||
Description: "",
|
||||
Amount: 1000,
|
||||
Amount: decimal.RequireFromString("10.00"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
|
|
@ -64,7 +65,7 @@ func TestExpenseService_CreateExpense_ZeroAmount(t *testing.T) {
|
|||
SpaceID: "some-space",
|
||||
UserID: "some-user",
|
||||
Description: "Something",
|
||||
Amount: 0,
|
||||
Amount: decimal.Zero,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
|
|
@ -87,7 +88,7 @@ func TestExpenseService_GetExpensesWithTagsForSpacePaginated(t *testing.T) {
|
|||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Bus fare",
|
||||
Amount: 250,
|
||||
Amount: decimal.RequireFromString("2.50"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
TagIDs: []string{tag.ID},
|
||||
|
|
@ -99,7 +100,7 @@ func TestExpenseService_GetExpensesWithTagsForSpacePaginated(t *testing.T) {
|
|||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Coffee",
|
||||
Amount: 500,
|
||||
Amount: decimal.RequireFromString("5.00"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
|
|
@ -132,12 +133,12 @@ func TestExpenseService_GetBalanceForSpace(t *testing.T) {
|
|||
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-balance@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Balance Space")
|
||||
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Topup", 10000, model.ExpenseTypeTopup)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Groceries", 3000, model.ExpenseTypeExpense)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Topup", decimal.RequireFromString("100.00"), model.ExpenseTypeTopup)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Groceries", decimal.RequireFromString("30.00"), model.ExpenseTypeExpense)
|
||||
|
||||
balance, err := svc.GetBalanceForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 7000, balance)
|
||||
assert.True(t, decimal.RequireFromString("70.00").Equal(balance))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +157,7 @@ func TestExpenseService_GetExpensesByTag(t *testing.T) {
|
|||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Dinner",
|
||||
Amount: 2500,
|
||||
Amount: decimal.RequireFromString("25.00"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
TagIDs: []string{tag.ID},
|
||||
|
|
@ -169,7 +170,7 @@ func TestExpenseService_GetExpensesByTag(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, summaries, 1)
|
||||
assert.Equal(t, tag.ID, summaries[0].TagID)
|
||||
assert.Equal(t, 2500, summaries[0].TotalAmount)
|
||||
assert.True(t, decimal.RequireFromString("25.00").Equal(summaries[0].TotalAmount))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +186,7 @@ func TestExpenseService_UpdateExpense(t *testing.T) {
|
|||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Old Description",
|
||||
Amount: 1000,
|
||||
Amount: decimal.RequireFromString("10.00"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
|
|
@ -195,13 +196,13 @@ func TestExpenseService_UpdateExpense(t *testing.T) {
|
|||
ID: created.ID,
|
||||
SpaceID: space.ID,
|
||||
Description: "New Description",
|
||||
Amount: 2000,
|
||||
Amount: decimal.RequireFromString("20.00"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Description", updated.Description)
|
||||
assert.Equal(t, 2000, updated.AmountCents)
|
||||
assert.True(t, decimal.RequireFromString("20.00").Equal(updated.Amount))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +218,7 @@ func TestExpenseService_DeleteExpense(t *testing.T) {
|
|||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Doomed Expense",
|
||||
Amount: 500,
|
||||
Amount: decimal.RequireFromString("5.00"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type CreateLoanDTO struct {
|
||||
|
|
@ -14,7 +15,7 @@ type CreateLoanDTO struct {
|
|||
UserID string
|
||||
Name string
|
||||
Description string
|
||||
OriginalAmount int
|
||||
OriginalAmount decimal.Decimal
|
||||
InterestRateBps int
|
||||
StartDate time.Time
|
||||
EndDate *time.Time
|
||||
|
|
@ -24,7 +25,7 @@ type UpdateLoanDTO struct {
|
|||
ID string
|
||||
Name string
|
||||
Description string
|
||||
OriginalAmount int
|
||||
OriginalAmount decimal.Decimal
|
||||
InterestRateBps int
|
||||
StartDate time.Time
|
||||
EndDate *time.Time
|
||||
|
|
@ -48,24 +49,24 @@ func (s *LoanService) CreateLoan(dto CreateLoanDTO) (*model.Loan, error) {
|
|||
if dto.Name == "" {
|
||||
return nil, fmt.Errorf("loan name cannot be empty")
|
||||
}
|
||||
if dto.OriginalAmount <= 0 {
|
||||
if dto.OriginalAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
loan := &model.Loan{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: dto.SpaceID,
|
||||
Name: dto.Name,
|
||||
Description: dto.Description,
|
||||
OriginalAmountCents: dto.OriginalAmount,
|
||||
InterestRateBps: dto.InterestRateBps,
|
||||
StartDate: dto.StartDate,
|
||||
EndDate: dto.EndDate,
|
||||
IsPaidOff: false,
|
||||
CreatedBy: dto.UserID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: dto.SpaceID,
|
||||
Name: dto.Name,
|
||||
Description: dto.Description,
|
||||
OriginalAmount: dto.OriginalAmount,
|
||||
InterestRateBps: dto.InterestRateBps,
|
||||
StartDate: dto.StartDate,
|
||||
EndDate: dto.EndDate,
|
||||
IsPaidOff: false,
|
||||
CreatedBy: dto.UserID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := s.loanRepo.Create(loan); err != nil {
|
||||
|
|
@ -95,10 +96,10 @@ func (s *LoanService) GetLoanWithSummary(id string) (*model.LoanWithPaymentSumma
|
|||
}
|
||||
|
||||
return &model.LoanWithPaymentSummary{
|
||||
Loan: *loan,
|
||||
TotalPaidCents: totalPaid,
|
||||
RemainingCents: loan.OriginalAmountCents - totalPaid,
|
||||
ReceiptCount: receiptCount,
|
||||
Loan: *loan,
|
||||
TotalPaid: totalPaid,
|
||||
Remaining: loan.OriginalAmount.Sub(totalPaid),
|
||||
ReceiptCount: receiptCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -154,10 +155,10 @@ func (s *LoanService) attachSummaries(loans []*model.Loan) ([]*model.LoanWithPay
|
|||
return nil, err
|
||||
}
|
||||
result[i] = &model.LoanWithPaymentSummary{
|
||||
Loan: *loan,
|
||||
TotalPaidCents: totalPaid,
|
||||
RemainingCents: loan.OriginalAmountCents - totalPaid,
|
||||
ReceiptCount: receiptCount,
|
||||
Loan: *loan,
|
||||
TotalPaid: totalPaid,
|
||||
Remaining: loan.OriginalAmount.Sub(totalPaid),
|
||||
ReceiptCount: receiptCount,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
|
@ -167,7 +168,7 @@ func (s *LoanService) UpdateLoan(dto UpdateLoanDTO) (*model.Loan, error) {
|
|||
if dto.Name == "" {
|
||||
return nil, fmt.Errorf("loan name cannot be empty")
|
||||
}
|
||||
if dto.OriginalAmount <= 0 {
|
||||
if dto.OriginalAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +179,7 @@ func (s *LoanService) UpdateLoan(dto UpdateLoanDTO) (*model.Loan, error) {
|
|||
|
||||
existing.Name = dto.Name
|
||||
existing.Description = dto.Description
|
||||
existing.OriginalAmountCents = dto.OriginalAmount
|
||||
existing.OriginalAmount = dto.OriginalAmount
|
||||
existing.InterestRateBps = dto.InterestRateBps
|
||||
existing.StartDate = dto.StartDate
|
||||
existing.EndDate = dto.EndDate
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type CreateMoneyAccountDTO struct {
|
||||
|
|
@ -23,7 +24,7 @@ type UpdateMoneyAccountDTO struct {
|
|||
|
||||
type CreateTransferDTO struct {
|
||||
AccountID string
|
||||
Amount int
|
||||
Amount decimal.Decimal
|
||||
Direction model.TransferDirection
|
||||
Note string
|
||||
CreatedBy string
|
||||
|
|
@ -77,7 +78,7 @@ func (s *MoneyAccountService) GetAccountsForSpace(spaceID string) ([]model.Money
|
|||
}
|
||||
result[i] = model.MoneyAccountWithBalance{
|
||||
MoneyAccount: *acct,
|
||||
BalanceCents: balance,
|
||||
Balance: balance,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,8 +114,8 @@ func (s *MoneyAccountService) DeleteAccount(id string) error {
|
|||
return s.accountRepo.Delete(id)
|
||||
}
|
||||
|
||||
func (s *MoneyAccountService) CreateTransfer(dto CreateTransferDTO, availableSpaceBalance int) (*model.AccountTransfer, error) {
|
||||
if dto.Amount <= 0 {
|
||||
func (s *MoneyAccountService) CreateTransfer(dto CreateTransferDTO, availableSpaceBalance decimal.Decimal) (*model.AccountTransfer, error) {
|
||||
if dto.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +124,7 @@ func (s *MoneyAccountService) CreateTransfer(dto CreateTransferDTO, availableSpa
|
|||
}
|
||||
|
||||
if dto.Direction == model.TransferDirectionDeposit {
|
||||
if dto.Amount > availableSpaceBalance {
|
||||
if dto.Amount.GreaterThan(availableSpaceBalance) {
|
||||
return nil, fmt.Errorf("insufficient available balance")
|
||||
}
|
||||
}
|
||||
|
|
@ -133,19 +134,19 @@ func (s *MoneyAccountService) CreateTransfer(dto CreateTransferDTO, availableSpa
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dto.Amount > accountBalance {
|
||||
if dto.Amount.GreaterThan(accountBalance) {
|
||||
return nil, fmt.Errorf("insufficient account balance")
|
||||
}
|
||||
}
|
||||
|
||||
transfer := &model.AccountTransfer{
|
||||
ID: uuid.NewString(),
|
||||
AccountID: dto.AccountID,
|
||||
AmountCents: dto.Amount,
|
||||
Direction: dto.Direction,
|
||||
Note: strings.TrimSpace(dto.Note),
|
||||
CreatedBy: dto.CreatedBy,
|
||||
CreatedAt: time.Now(),
|
||||
ID: uuid.NewString(),
|
||||
AccountID: dto.AccountID,
|
||||
Amount: dto.Amount,
|
||||
Direction: dto.Direction,
|
||||
Note: strings.TrimSpace(dto.Note),
|
||||
CreatedBy: dto.CreatedBy,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := s.accountRepo.CreateTransfer(transfer)
|
||||
|
|
@ -164,11 +165,11 @@ func (s *MoneyAccountService) DeleteTransfer(id string) error {
|
|||
return s.accountRepo.DeleteTransfer(id)
|
||||
}
|
||||
|
||||
func (s *MoneyAccountService) GetAccountBalance(accountID string) (int, error) {
|
||||
func (s *MoneyAccountService) GetAccountBalance(accountID string) (decimal.Decimal, error) {
|
||||
return s.accountRepo.GetAccountBalance(accountID)
|
||||
}
|
||||
|
||||
func (s *MoneyAccountService) GetTotalAllocatedForSpace(spaceID string) (int, error) {
|
||||
func (s *MoneyAccountService) GetTotalAllocatedForSpace(spaceID string) (decimal.Decimal, error) {
|
||||
return s.accountRepo.GetTotalAllocatedForSpace(spaceID)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -53,13 +54,13 @@ func TestMoneyAccountService_GetAccountsForSpace(t *testing.T) {
|
|||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-list@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc List Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Checking", user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, 5000, model.TransferDirectionDeposit, user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, decimal.RequireFromString("50.00"), model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
accounts, err := svc.GetAccountsForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, accounts, 1)
|
||||
assert.Equal(t, "Checking", accounts[0].Name)
|
||||
assert.Equal(t, 5000, accounts[0].BalanceCents)
|
||||
assert.True(t, decimal.RequireFromString("50.00").Equal(accounts[0].Balance))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -74,14 +75,14 @@ func TestMoneyAccountService_CreateTransfer_Deposit(t *testing.T) {
|
|||
|
||||
transfer, err := svc.CreateTransfer(CreateTransferDTO{
|
||||
AccountID: account.ID,
|
||||
Amount: 3000,
|
||||
Amount: decimal.RequireFromString("30.00"),
|
||||
Direction: model.TransferDirectionDeposit,
|
||||
Note: "Initial deposit",
|
||||
CreatedBy: user.ID,
|
||||
}, 10000)
|
||||
}, decimal.RequireFromString("100.00"))
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, transfer.ID)
|
||||
assert.Equal(t, 3000, transfer.AmountCents)
|
||||
assert.True(t, decimal.RequireFromString("30.00").Equal(transfer.Amount))
|
||||
assert.Equal(t, model.TransferDirectionDeposit, transfer.Direction)
|
||||
})
|
||||
}
|
||||
|
|
@ -97,11 +98,11 @@ func TestMoneyAccountService_CreateTransfer_InsufficientBalance(t *testing.T) {
|
|||
|
||||
transfer, err := svc.CreateTransfer(CreateTransferDTO{
|
||||
AccountID: account.ID,
|
||||
Amount: 5000,
|
||||
Amount: decimal.RequireFromString("50.00"),
|
||||
Direction: model.TransferDirectionDeposit,
|
||||
Note: "Too much",
|
||||
CreatedBy: user.ID,
|
||||
}, 1000)
|
||||
}, decimal.RequireFromString("10.00"))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, transfer)
|
||||
})
|
||||
|
|
@ -115,18 +116,18 @@ func TestMoneyAccountService_CreateTransfer_Withdrawal(t *testing.T) {
|
|||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-withdraw@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Withdraw Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Withdraw Account", user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, 5000, model.TransferDirectionDeposit, user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, decimal.RequireFromString("50.00"), model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
transfer, err := svc.CreateTransfer(CreateTransferDTO{
|
||||
AccountID: account.ID,
|
||||
Amount: 2000,
|
||||
Amount: decimal.RequireFromString("20.00"),
|
||||
Direction: model.TransferDirectionWithdrawal,
|
||||
Note: "Withdrawal",
|
||||
CreatedBy: user.ID,
|
||||
}, 0)
|
||||
}, decimal.Zero)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, transfer.ID)
|
||||
assert.Equal(t, 2000, transfer.AmountCents)
|
||||
assert.True(t, decimal.RequireFromString("20.00").Equal(transfer.Amount))
|
||||
assert.Equal(t, model.TransferDirectionWithdrawal, transfer.Direction)
|
||||
})
|
||||
}
|
||||
|
|
@ -140,14 +141,14 @@ func TestMoneyAccountService_GetTotalAllocatedForSpace(t *testing.T) {
|
|||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Total Space")
|
||||
|
||||
account1 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account 1", user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account1.ID, 3000, model.TransferDirectionDeposit, user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account1.ID, decimal.RequireFromString("30.00"), model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
account2 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account 2", user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account2.ID, 2000, model.TransferDirectionDeposit, user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account2.ID, decimal.RequireFromString("20.00"), model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
total, err := svc.GetTotalAllocatedForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5000, total)
|
||||
assert.True(t, decimal.RequireFromString("50.00").Equal(total))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +178,7 @@ func TestMoneyAccountService_DeleteTransfer(t *testing.T) {
|
|||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-deltx@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc DelTx Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "DelTx Account", user.ID)
|
||||
transfer := testutil.CreateTestTransfer(t, dbi.DB, account.ID, 1000, model.TransferDirectionDeposit, user.ID)
|
||||
transfer := testutil.CreateTestTransfer(t, dbi.DB, account.ID, decimal.RequireFromString("10.00"), model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
err := svc.DeleteTransfer(transfer.ID)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type FundingSourceDTO struct {
|
||||
SourceType model.FundingSourceType
|
||||
AccountID string
|
||||
Amount int
|
||||
Amount decimal.Decimal
|
||||
}
|
||||
|
||||
type CreateReceiptDTO struct {
|
||||
|
|
@ -20,7 +21,7 @@ type CreateReceiptDTO struct {
|
|||
SpaceID string
|
||||
UserID string
|
||||
Description string
|
||||
TotalAmount int
|
||||
TotalAmount decimal.Decimal
|
||||
Date time.Time
|
||||
FundingSources []FundingSourceDTO
|
||||
RecurringReceiptID *string
|
||||
|
|
@ -31,7 +32,7 @@ type UpdateReceiptDTO struct {
|
|||
SpaceID string
|
||||
UserID string
|
||||
Description string
|
||||
TotalAmount int
|
||||
TotalAmount decimal.Decimal
|
||||
Date time.Time
|
||||
FundingSources []FundingSourceDTO
|
||||
}
|
||||
|
|
@ -57,7 +58,7 @@ func NewReceiptService(
|
|||
}
|
||||
|
||||
func (s *ReceiptService) CreateReceipt(dto CreateReceiptDTO) (*model.ReceiptWithSources, error) {
|
||||
if dto.TotalAmount <= 0 {
|
||||
if dto.TotalAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
if len(dto.FundingSources) == 0 {
|
||||
|
|
@ -65,15 +66,15 @@ func (s *ReceiptService) CreateReceipt(dto CreateReceiptDTO) (*model.ReceiptWith
|
|||
}
|
||||
|
||||
// Validate funding sources sum to total
|
||||
var sum int
|
||||
sum := decimal.Zero
|
||||
for _, src := range dto.FundingSources {
|
||||
if src.Amount <= 0 {
|
||||
if src.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("each funding source amount must be positive")
|
||||
}
|
||||
sum += src.Amount
|
||||
sum = sum.Add(src.Amount)
|
||||
}
|
||||
if sum != dto.TotalAmount {
|
||||
return nil, fmt.Errorf("funding source amounts (%d) must equal total amount (%d)", sum, dto.TotalAmount)
|
||||
if !sum.Equal(dto.TotalAmount) {
|
||||
return nil, fmt.Errorf("funding source amounts (%s) must equal total amount (%s)", sum, dto.TotalAmount)
|
||||
}
|
||||
|
||||
// Validate loan exists and is not paid off
|
||||
|
|
@ -91,7 +92,7 @@ func (s *ReceiptService) CreateReceipt(dto CreateReceiptDTO) (*model.ReceiptWith
|
|||
LoanID: dto.LoanID,
|
||||
SpaceID: dto.SpaceID,
|
||||
Description: dto.Description,
|
||||
TotalAmountCents: dto.TotalAmount,
|
||||
TotalAmount: dto.TotalAmount,
|
||||
Date: dto.Date,
|
||||
RecurringReceiptID: dto.RecurringReceiptID,
|
||||
CreatedBy: dto.UserID,
|
||||
|
|
@ -107,7 +108,7 @@ func (s *ReceiptService) CreateReceipt(dto CreateReceiptDTO) (*model.ReceiptWith
|
|||
|
||||
// Check if loan is now fully paid off
|
||||
totalPaid, err := s.loanRepo.GetTotalPaidForLoan(dto.LoanID)
|
||||
if err == nil && totalPaid >= loan.OriginalAmountCents {
|
||||
if err == nil && totalPaid.GreaterThanOrEqual(loan.OriginalAmount) {
|
||||
_ = s.loanRepo.SetPaidOff(loan.ID, true)
|
||||
}
|
||||
|
||||
|
|
@ -127,10 +128,10 @@ func (s *ReceiptService) buildLinkedRecords(
|
|||
|
||||
for _, src := range fundingSources {
|
||||
fs := model.ReceiptFundingSource{
|
||||
ID: uuid.NewString(),
|
||||
ReceiptID: receipt.ID,
|
||||
SourceType: src.SourceType,
|
||||
AmountCents: src.Amount,
|
||||
ID: uuid.NewString(),
|
||||
ReceiptID: receipt.ID,
|
||||
SourceType: src.SourceType,
|
||||
Amount: src.Amount,
|
||||
}
|
||||
|
||||
if src.SourceType == model.FundingSourceBalance {
|
||||
|
|
@ -139,7 +140,7 @@ func (s *ReceiptService) buildLinkedRecords(
|
|||
SpaceID: spaceID,
|
||||
CreatedBy: userID,
|
||||
Description: fmt.Sprintf("Loan payment: %s", description),
|
||||
AmountCents: src.Amount,
|
||||
Amount: src.Amount,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: date,
|
||||
CreatedAt: now,
|
||||
|
|
@ -151,13 +152,13 @@ func (s *ReceiptService) buildLinkedRecords(
|
|||
acctID := src.AccountID
|
||||
fs.AccountID = &acctID
|
||||
transfer := &model.AccountTransfer{
|
||||
ID: uuid.NewString(),
|
||||
AccountID: src.AccountID,
|
||||
AmountCents: src.Amount,
|
||||
Direction: model.TransferDirectionWithdrawal,
|
||||
Note: fmt.Sprintf("Loan payment: %s", description),
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
ID: uuid.NewString(),
|
||||
AccountID: src.AccountID,
|
||||
Amount: src.Amount,
|
||||
Direction: model.TransferDirectionWithdrawal,
|
||||
Note: fmt.Sprintf("Loan payment: %s", description),
|
||||
CreatedBy: userID,
|
||||
CreatedAt: now,
|
||||
}
|
||||
accountTransfers = append(accountTransfers, transfer)
|
||||
fs.LinkedTransferID = &transfer.ID
|
||||
|
|
@ -255,7 +256,7 @@ func (s *ReceiptService) DeleteReceipt(id string, spaceID string) error {
|
|||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if loan.IsPaidOff && totalPaid < loan.OriginalAmountCents {
|
||||
if loan.IsPaidOff && totalPaid.LessThan(loan.OriginalAmount) {
|
||||
_ = s.loanRepo.SetPaidOff(loan.ID, false)
|
||||
}
|
||||
|
||||
|
|
@ -263,22 +264,22 @@ func (s *ReceiptService) DeleteReceipt(id string, spaceID string) error {
|
|||
}
|
||||
|
||||
func (s *ReceiptService) UpdateReceipt(dto UpdateReceiptDTO) (*model.ReceiptWithSources, error) {
|
||||
if dto.TotalAmount <= 0 {
|
||||
if dto.TotalAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
if len(dto.FundingSources) == 0 {
|
||||
return nil, fmt.Errorf("at least one funding source is required")
|
||||
}
|
||||
|
||||
var sum int
|
||||
sum := decimal.Zero
|
||||
for _, src := range dto.FundingSources {
|
||||
if src.Amount <= 0 {
|
||||
if src.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("each funding source amount must be positive")
|
||||
}
|
||||
sum += src.Amount
|
||||
sum = sum.Add(src.Amount)
|
||||
}
|
||||
if sum != dto.TotalAmount {
|
||||
return nil, fmt.Errorf("funding source amounts (%d) must equal total amount (%d)", sum, dto.TotalAmount)
|
||||
if !sum.Equal(dto.TotalAmount) {
|
||||
return nil, fmt.Errorf("funding source amounts (%s) must equal total amount (%s)", sum, dto.TotalAmount)
|
||||
}
|
||||
|
||||
existing, err := s.receiptRepo.GetByID(dto.ID)
|
||||
|
|
@ -290,7 +291,7 @@ func (s *ReceiptService) UpdateReceipt(dto UpdateReceiptDTO) (*model.ReceiptWith
|
|||
}
|
||||
|
||||
existing.Description = dto.Description
|
||||
existing.TotalAmountCents = dto.TotalAmount
|
||||
existing.TotalAmount = dto.TotalAmount
|
||||
existing.Date = dto.Date
|
||||
existing.UpdatedAt = time.Now()
|
||||
|
||||
|
|
@ -305,9 +306,9 @@ func (s *ReceiptService) UpdateReceipt(dto UpdateReceiptDTO) (*model.ReceiptWith
|
|||
if err == nil {
|
||||
totalPaid, err := s.loanRepo.GetTotalPaidForLoan(existing.LoanID)
|
||||
if err == nil {
|
||||
if totalPaid >= loan.OriginalAmountCents && !loan.IsPaidOff {
|
||||
if totalPaid.GreaterThanOrEqual(loan.OriginalAmount) && !loan.IsPaidOff {
|
||||
_ = s.loanRepo.SetPaidOff(loan.ID, true)
|
||||
} else if totalPaid < loan.OriginalAmountCents && loan.IsPaidOff {
|
||||
} else if totalPaid.LessThan(loan.OriginalAmount) && loan.IsPaidOff {
|
||||
_ = s.loanRepo.SetPaidOff(loan.ID, false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type CreateRecurringExpenseDTO struct {
|
||||
SpaceID string
|
||||
UserID string
|
||||
Description string
|
||||
Amount int
|
||||
Amount decimal.Decimal
|
||||
Type model.ExpenseType
|
||||
PaymentMethodID *string
|
||||
Frequency model.Frequency
|
||||
|
|
@ -26,7 +27,7 @@ type CreateRecurringExpenseDTO struct {
|
|||
type UpdateRecurringExpenseDTO struct {
|
||||
ID string
|
||||
Description string
|
||||
Amount int
|
||||
Amount decimal.Decimal
|
||||
Type model.ExpenseType
|
||||
PaymentMethodID *string
|
||||
Frequency model.Frequency
|
||||
|
|
@ -55,7 +56,7 @@ func (s *RecurringExpenseService) CreateRecurringExpense(dto CreateRecurringExpe
|
|||
if dto.Description == "" {
|
||||
return nil, fmt.Errorf("description cannot be empty")
|
||||
}
|
||||
if dto.Amount <= 0 {
|
||||
if dto.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +66,7 @@ func (s *RecurringExpenseService) CreateRecurringExpense(dto CreateRecurringExpe
|
|||
SpaceID: dto.SpaceID,
|
||||
CreatedBy: dto.UserID,
|
||||
Description: dto.Description,
|
||||
AmountCents: dto.Amount,
|
||||
Amount: dto.Amount,
|
||||
Type: dto.Type,
|
||||
PaymentMethodID: dto.PaymentMethodID,
|
||||
Frequency: dto.Frequency,
|
||||
|
|
@ -127,7 +128,7 @@ func (s *RecurringExpenseService) UpdateRecurringExpense(dto UpdateRecurringExpe
|
|||
if dto.Description == "" {
|
||||
return nil, fmt.Errorf("description cannot be empty")
|
||||
}
|
||||
if dto.Amount <= 0 {
|
||||
if dto.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +138,7 @@ func (s *RecurringExpenseService) UpdateRecurringExpense(dto UpdateRecurringExpe
|
|||
}
|
||||
|
||||
existing.Description = dto.Description
|
||||
existing.AmountCents = dto.Amount
|
||||
existing.Amount = dto.Amount
|
||||
existing.Type = dto.Type
|
||||
existing.PaymentMethodID = dto.PaymentMethodID
|
||||
existing.Frequency = dto.Frequency
|
||||
|
|
@ -229,7 +230,7 @@ func (s *RecurringExpenseService) processRecurrence(re *model.RecurringExpense,
|
|||
SpaceID: re.SpaceID,
|
||||
CreatedBy: re.CreatedBy,
|
||||
Description: re.Description,
|
||||
AmountCents: re.AmountCents,
|
||||
Amount: re.Amount,
|
||||
Type: re.Type,
|
||||
Date: re.NextOccurrence,
|
||||
PaymentMethodID: re.PaymentMethodID,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type CreateRecurringReceiptDTO struct {
|
||||
|
|
@ -15,7 +16,7 @@ type CreateRecurringReceiptDTO struct {
|
|||
SpaceID string
|
||||
UserID string
|
||||
Description string
|
||||
TotalAmount int
|
||||
TotalAmount decimal.Decimal
|
||||
Frequency model.Frequency
|
||||
StartDate time.Time
|
||||
EndDate *time.Time
|
||||
|
|
@ -25,7 +26,7 @@ type CreateRecurringReceiptDTO struct {
|
|||
type UpdateRecurringReceiptDTO struct {
|
||||
ID string
|
||||
Description string
|
||||
TotalAmount int
|
||||
TotalAmount decimal.Decimal
|
||||
Frequency model.Frequency
|
||||
StartDate time.Time
|
||||
EndDate *time.Time
|
||||
|
|
@ -57,36 +58,36 @@ func NewRecurringReceiptService(
|
|||
}
|
||||
|
||||
func (s *RecurringReceiptService) CreateRecurringReceipt(dto CreateRecurringReceiptDTO) (*model.RecurringReceiptWithSources, error) {
|
||||
if dto.TotalAmount <= 0 {
|
||||
if dto.TotalAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
if len(dto.FundingSources) == 0 {
|
||||
return nil, fmt.Errorf("at least one funding source is required")
|
||||
}
|
||||
|
||||
var sum int
|
||||
sum := decimal.Zero
|
||||
for _, src := range dto.FundingSources {
|
||||
sum += src.Amount
|
||||
sum = sum.Add(src.Amount)
|
||||
}
|
||||
if sum != dto.TotalAmount {
|
||||
if !sum.Equal(dto.TotalAmount) {
|
||||
return nil, fmt.Errorf("funding source amounts must equal total amount")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
rr := &model.RecurringReceipt{
|
||||
ID: uuid.NewString(),
|
||||
LoanID: dto.LoanID,
|
||||
SpaceID: dto.SpaceID,
|
||||
Description: dto.Description,
|
||||
TotalAmountCents: dto.TotalAmount,
|
||||
Frequency: dto.Frequency,
|
||||
StartDate: dto.StartDate,
|
||||
EndDate: dto.EndDate,
|
||||
NextOccurrence: dto.StartDate,
|
||||
IsActive: true,
|
||||
CreatedBy: dto.UserID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ID: uuid.NewString(),
|
||||
LoanID: dto.LoanID,
|
||||
SpaceID: dto.SpaceID,
|
||||
Description: dto.Description,
|
||||
TotalAmount: dto.TotalAmount,
|
||||
Frequency: dto.Frequency,
|
||||
StartDate: dto.StartDate,
|
||||
EndDate: dto.EndDate,
|
||||
NextOccurrence: dto.StartDate,
|
||||
IsActive: true,
|
||||
CreatedBy: dto.UserID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
sources := make([]model.RecurringReceiptSource, len(dto.FundingSources))
|
||||
|
|
@ -95,7 +96,7 @@ func (s *RecurringReceiptService) CreateRecurringReceipt(dto CreateRecurringRece
|
|||
ID: uuid.NewString(),
|
||||
RecurringReceiptID: rr.ID,
|
||||
SourceType: src.SourceType,
|
||||
AmountCents: src.Amount,
|
||||
Amount: src.Amount,
|
||||
}
|
||||
if src.SourceType == model.FundingSourceAccount {
|
||||
acctID := src.AccountID
|
||||
|
|
@ -142,7 +143,7 @@ func (s *RecurringReceiptService) GetRecurringReceiptsWithSourcesForLoan(loanID
|
|||
}
|
||||
|
||||
func (s *RecurringReceiptService) UpdateRecurringReceipt(dto UpdateRecurringReceiptDTO) (*model.RecurringReceipt, error) {
|
||||
if dto.TotalAmount <= 0 {
|
||||
if dto.TotalAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return nil, fmt.Errorf("amount must be positive")
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +153,7 @@ func (s *RecurringReceiptService) UpdateRecurringReceipt(dto UpdateRecurringRece
|
|||
}
|
||||
|
||||
existing.Description = dto.Description
|
||||
existing.TotalAmountCents = dto.TotalAmount
|
||||
existing.TotalAmount = dto.TotalAmount
|
||||
existing.Frequency = dto.Frequency
|
||||
existing.StartDate = dto.StartDate
|
||||
existing.EndDate = dto.EndDate
|
||||
|
|
@ -168,7 +169,7 @@ func (s *RecurringReceiptService) UpdateRecurringReceipt(dto UpdateRecurringRece
|
|||
ID: uuid.NewString(),
|
||||
RecurringReceiptID: existing.ID,
|
||||
SourceType: src.SourceType,
|
||||
AmountCents: src.Amount,
|
||||
Amount: src.Amount,
|
||||
}
|
||||
if src.SourceType == model.FundingSourceAccount {
|
||||
acctID := src.AccountID
|
||||
|
|
@ -262,7 +263,7 @@ func (s *RecurringReceiptService) processRecurrence(rr *model.RecurringReceipt,
|
|||
fundingSources[i] = FundingSourceDTO{
|
||||
SourceType: src.SourceType,
|
||||
AccountID: accountID,
|
||||
Amount: src.AmountCents,
|
||||
Amount: src.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +273,7 @@ func (s *RecurringReceiptService) processRecurrence(rr *model.RecurringReceipt,
|
|||
SpaceID: rr.SpaceID,
|
||||
UserID: rr.CreatedBy,
|
||||
Description: rr.Description,
|
||||
TotalAmount: rr.TotalAmountCents,
|
||||
TotalAmount: rr.TotalAmount,
|
||||
Date: rr.NextOccurrence,
|
||||
FundingSources: fundingSources,
|
||||
RecurringReceiptID: &rrID,
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func (s *ReportService) GetSpendingReport(spaceID string, from, to time.Time) (*
|
|||
TopExpenses: topWithTags,
|
||||
TotalIncome: totalIncome,
|
||||
TotalExpenses: totalExpenses,
|
||||
NetBalance: totalIncome - totalExpenses,
|
||||
NetBalance: totalIncome.Sub(totalExpenses),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue