budgit/internal/service/expense_test.go
2026-03-14 17:56:25 -04:00

233 lines
7.7 KiB
Go

package service
import (
"testing"
"time"
"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"
)
func TestExpenseService_CreateExpense(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-create@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Space")
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Food", nil)
expense, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Lunch",
Amount: decimal.RequireFromString("15.49"),
Type: model.ExpenseTypeExpense,
Date: time.Now(),
TagIDs: []string{tag.ID},
})
require.NoError(t, err)
assert.NotEmpty(t, expense.ID)
assert.Equal(t, "Lunch", expense.Description)
assert.True(t, decimal.RequireFromString("15.49").Equal(expense.Amount))
assert.Equal(t, model.ExpenseTypeExpense, expense.Type)
})
}
func TestExpenseService_CreateExpense_EmptyDescription(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
expense, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: "some-space",
UserID: "some-user",
Description: "",
Amount: decimal.RequireFromString("10.75"),
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
assert.Error(t, err)
assert.Nil(t, expense)
})
}
func TestExpenseService_CreateExpense_ZeroAmount(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
expense, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: "some-space",
UserID: "some-user",
Description: "Something",
Amount: decimal.Zero,
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
assert.Error(t, err)
assert.Nil(t, expense)
})
}
func TestExpenseService_GetExpensesWithTagsForSpacePaginated(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-paginate@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Paginate Space")
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Transport", nil)
// Create expense with tag via the service
_, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Bus fare",
Amount: decimal.RequireFromString("2.49"),
Type: model.ExpenseTypeExpense,
Date: time.Now(),
TagIDs: []string{tag.ID},
})
require.NoError(t, err)
// Create expense without tag
_, err = svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Coffee",
Amount: decimal.RequireFromString("5.01"),
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
require.NoError(t, err)
results, totalPages, err := svc.GetExpensesWithTagsForSpacePaginated(space.ID, 1)
require.NoError(t, err)
assert.Len(t, results, 2)
assert.Equal(t, 1, totalPages)
// Verify at least one result has tags and one does not
var withTags, withoutTags int
for _, r := range results {
if len(r.Tags) > 0 {
withTags++
} else {
withoutTags++
}
}
assert.Equal(t, 1, withTags)
assert.Equal(t, 1, withoutTags)
})
}
func TestExpenseService_GetBalanceForSpace(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
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", decimal.RequireFromString("100.50"), model.ExpenseTypeTopup)
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Groceries", decimal.RequireFromString("30.75"), model.ExpenseTypeExpense)
balance, err := svc.GetBalanceForSpace(space.ID)
require.NoError(t, err)
assert.True(t, decimal.RequireFromString("69.75").Equal(balance))
})
}
func TestExpenseService_GetExpensesByTag(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-bytag@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc ByTag Space")
tagColor := "#ff0000"
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Dining", &tagColor)
now := time.Now()
_, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Dinner",
Amount: decimal.RequireFromString("24.99"),
Type: model.ExpenseTypeExpense,
Date: now,
TagIDs: []string{tag.ID},
})
require.NoError(t, err)
fromDate := now.Add(-24 * time.Hour)
toDate := now.Add(24 * time.Hour)
summaries, err := svc.GetExpensesByTag(space.ID, fromDate, toDate)
require.NoError(t, err)
require.Len(t, summaries, 1)
assert.Equal(t, tag.ID, summaries[0].TagID)
assert.True(t, decimal.RequireFromString("24.99").Equal(summaries[0].TotalAmount))
})
}
func TestExpenseService_UpdateExpense(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-update@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Update Space")
created, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Old Description",
Amount: decimal.RequireFromString("10.75"),
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
require.NoError(t, err)
updated, err := svc.UpdateExpense(UpdateExpenseDTO{
ID: created.ID,
SpaceID: space.ID,
Description: "New Description",
Amount: decimal.RequireFromString("19.49"),
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
require.NoError(t, err)
assert.Equal(t, "New Description", updated.Description)
assert.True(t, decimal.RequireFromString("19.49").Equal(updated.Amount))
})
}
func TestExpenseService_DeleteExpense(t *testing.T) {
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
expenseRepo := repository.NewExpenseRepository(dbi.DB)
svc := NewExpenseService(expenseRepo)
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-delete@example.com", nil)
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Delete Space")
created, err := svc.CreateExpense(CreateExpenseDTO{
SpaceID: space.ID,
UserID: user.ID,
Description: "Doomed Expense",
Amount: decimal.RequireFromString("4.99"),
Type: model.ExpenseTypeExpense,
Date: time.Now(),
})
require.NoError(t, err)
err = svc.DeleteExpense(created.ID, space.ID)
require.NoError(t, err)
_, err = svc.GetExpense(created.ID)
assert.Error(t, err)
})
}