chore: massive reset
This commit is contained in:
parent
c7ee3da8f2
commit
df164ab0f4
96 changed files with 198 additions and 15405 deletions
|
|
@ -1,168 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBudgetNotFound = errors.New("budget not found")
|
||||
)
|
||||
|
||||
type BudgetRepository interface {
|
||||
Create(budget *model.Budget, tagIDs []string) error
|
||||
GetByID(id string) (*model.Budget, error)
|
||||
GetBySpaceID(spaceID string) ([]*model.Budget, error)
|
||||
GetSpentForBudget(spaceID string, tagIDs []string, periodStart, periodEnd time.Time) (decimal.Decimal, error)
|
||||
GetTagsByBudgetIDs(budgetIDs []string) (map[string][]*model.Tag, error)
|
||||
Update(budget *model.Budget, tagIDs []string) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type budgetRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewBudgetRepository(db *sqlx.DB) BudgetRepository {
|
||||
return &budgetRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *budgetRepository) Create(budget *model.Budget, tagIDs []string) error {
|
||||
return WithTx(r.db, func(tx *sqlx.Tx) error {
|
||||
query := `INSERT INTO budgets (id, space_id, amount, period, start_date, end_date, is_active, created_by, created_at, updated_at, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 0);`
|
||||
if _, err := tx.Exec(query, budget.ID, budget.SpaceID, budget.Amount, budget.Period, budget.StartDate, budget.EndDate, budget.IsActive, budget.CreatedBy, budget.CreatedAt, budget.UpdatedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tagIDs) > 0 {
|
||||
tagQuery := `INSERT INTO budget_tags (budget_id, tag_id) VALUES ($1, $2);`
|
||||
for _, tagID := range tagIDs {
|
||||
if _, err := tx.Exec(tagQuery, budget.ID, tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *budgetRepository) GetByID(id string) (*model.Budget, error) {
|
||||
budget := &model.Budget{}
|
||||
err := r.db.Get(budget, `SELECT * FROM budgets WHERE id = $1;`, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrBudgetNotFound
|
||||
}
|
||||
return budget, err
|
||||
}
|
||||
|
||||
func (r *budgetRepository) GetBySpaceID(spaceID string) ([]*model.Budget, error) {
|
||||
var budgets []*model.Budget
|
||||
err := r.db.Select(&budgets, `SELECT * FROM budgets WHERE space_id = $1 ORDER BY created_at DESC;`, spaceID)
|
||||
return budgets, err
|
||||
}
|
||||
|
||||
func (r *budgetRepository) GetSpentForBudget(spaceID string, tagIDs []string, periodStart, periodEnd time.Time) (decimal.Decimal, error) {
|
||||
if len(tagIDs) == 0 {
|
||||
return decimal.Zero, nil
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(`
|
||||
SELECT COALESCE(SUM(CAST(e.amount AS DECIMAL)), 0)
|
||||
FROM expenses e
|
||||
WHERE e.space_id = ? AND e.type = 'expense' AND e.date >= ? AND e.date <= ?
|
||||
AND EXISTS (SELECT 1 FROM expense_tags et WHERE et.expense_id = e.id AND et.tag_id IN (?))
|
||||
`, spaceID, periodStart, periodEnd, tagIDs)
|
||||
if err != nil {
|
||||
return decimal.Zero, err
|
||||
}
|
||||
query = r.db.Rebind(query)
|
||||
|
||||
var spent decimal.Decimal
|
||||
err = r.db.Get(&spent, query, args...)
|
||||
return spent, err
|
||||
}
|
||||
|
||||
func (r *budgetRepository) GetTagsByBudgetIDs(budgetIDs []string) (map[string][]*model.Tag, error) {
|
||||
if len(budgetIDs) == 0 {
|
||||
return make(map[string][]*model.Tag), nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
BudgetID string `db:"budget_id"`
|
||||
ID string `db:"id"`
|
||||
SpaceID string `db:"space_id"`
|
||||
Name string `db:"name"`
|
||||
Color *string `db:"color"`
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(`
|
||||
SELECT bt.budget_id, t.id, t.space_id, t.name, t.color
|
||||
FROM budget_tags bt
|
||||
JOIN tags t ON bt.tag_id = t.id
|
||||
WHERE bt.budget_id IN (?)
|
||||
ORDER BY t.name;
|
||||
`, budgetIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = r.db.Rebind(query)
|
||||
|
||||
var rows []row
|
||||
if err := r.db.Select(&rows, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string][]*model.Tag)
|
||||
for _, rw := range rows {
|
||||
result[rw.BudgetID] = append(result[rw.BudgetID], &model.Tag{
|
||||
ID: rw.ID,
|
||||
SpaceID: rw.SpaceID,
|
||||
Name: rw.Name,
|
||||
Color: rw.Color,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *budgetRepository) Update(budget *model.Budget, tagIDs []string) error {
|
||||
return WithTx(r.db, func(tx *sqlx.Tx) error {
|
||||
query := `UPDATE budgets SET amount = $1, period = $2, start_date = $3, end_date = $4, is_active = $5, updated_at = $6 WHERE id = $7;`
|
||||
if _, err := tx.Exec(query, budget.Amount, budget.Period, budget.StartDate, budget.EndDate, budget.IsActive, budget.UpdatedAt, budget.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM budget_tags WHERE budget_id = $1;`, budget.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tagIDs) > 0 {
|
||||
tagQuery := `INSERT INTO budget_tags (budget_id, tag_id) VALUES ($1, $2);`
|
||||
for _, tagID := range tagIDs {
|
||||
if _, err := tx.Exec(tagQuery, budget.ID, tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *budgetRepository) Delete(id string) error {
|
||||
result, err := r.db.Exec(`DELETE FROM budgets WHERE id = $1;`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrBudgetNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,347 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrExpenseNotFound = errors.New("expense not found")
|
||||
)
|
||||
|
||||
type ExpenseRepository interface {
|
||||
Create(expense *model.Expense, tagIDs []string, itemIDs []string) error
|
||||
GetByID(id string) (*model.Expense, error)
|
||||
GetBySpaceID(spaceID string) ([]*model.Expense, error)
|
||||
GetBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.Expense, error)
|
||||
CountBySpaceID(spaceID string) (int, error)
|
||||
GetExpensesByTag(spaceID string, fromDate, toDate time.Time) ([]*model.TagExpenseSummary, error)
|
||||
GetTagsByExpenseIDs(expenseIDs []string) (map[string][]*model.Tag, error)
|
||||
GetPaymentMethodsByExpenseIDs(expenseIDs []string) (map[string]*model.PaymentMethod, error)
|
||||
Update(expense *model.Expense, tagIDs []string) error
|
||||
Delete(id string) error
|
||||
// Report queries
|
||||
GetDailySpending(spaceID string, from, to time.Time) ([]*model.DailySpending, error)
|
||||
GetMonthlySpending(spaceID string, from, to time.Time) ([]*model.MonthlySpending, error)
|
||||
GetTopExpenses(spaceID string, from, to time.Time, limit int) ([]*model.Expense, error)
|
||||
GetIncomeVsExpenseSummary(spaceID string, from, to time.Time) (decimal.Decimal, decimal.Decimal, error)
|
||||
GetExpensesByPaymentMethod(spaceID string, from, to time.Time) ([]*model.PaymentMethodExpenseSummary, error)
|
||||
}
|
||||
|
||||
type expenseRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewExpenseRepository(db *sqlx.DB) ExpenseRepository {
|
||||
return &expenseRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *expenseRepository) Create(expense *model.Expense, tagIDs []string, itemIDs []string) error {
|
||||
return WithTx(r.db, func(tx *sqlx.Tx) error {
|
||||
queryExpense := `INSERT INTO expenses (id, space_id, created_by, description, amount, type, date, payment_method_id, recurring_expense_id, created_at, updated_at, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 0);`
|
||||
if _, err := tx.Exec(queryExpense, expense.ID, expense.SpaceID, expense.CreatedBy, expense.Description, expense.Amount, expense.Type, expense.Date, expense.PaymentMethodID, expense.RecurringExpenseID, expense.CreatedAt, expense.UpdatedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tagIDs) > 0 {
|
||||
queryTags := `INSERT INTO expense_tags (expense_id, tag_id) VALUES ($1, $2);`
|
||||
for _, tagID := range tagIDs {
|
||||
if _, err := tx.Exec(queryTags, expense.ID, tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(itemIDs) > 0 {
|
||||
queryItems := `INSERT INTO expense_items (expense_id, item_id) VALUES ($1, $2);`
|
||||
for _, itemID := range itemIDs {
|
||||
if _, err := tx.Exec(queryItems, expense.ID, itemID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetByID(id string) (*model.Expense, error) {
|
||||
expense := &model.Expense{}
|
||||
query := `SELECT * FROM expenses WHERE id = $1;`
|
||||
err := r.db.Get(expense, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrExpenseNotFound
|
||||
}
|
||||
return expense, err
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetBySpaceID(spaceID string) ([]*model.Expense, error) {
|
||||
var expenses []*model.Expense
|
||||
query := `SELECT * FROM expenses WHERE space_id = $1 ORDER BY date DESC, created_at DESC;`
|
||||
err := r.db.Select(&expenses, query, spaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return expenses, nil
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.Expense, error) {
|
||||
var expenses []*model.Expense
|
||||
query := `SELECT * FROM expenses WHERE space_id = $1 ORDER BY date DESC, created_at DESC LIMIT $2 OFFSET $3;`
|
||||
err := r.db.Select(&expenses, query, spaceID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return expenses, nil
|
||||
}
|
||||
|
||||
func (r *expenseRepository) CountBySpaceID(spaceID string) (int, error) {
|
||||
var count int
|
||||
err := r.db.Get(&count, `SELECT COUNT(*) FROM expenses WHERE space_id = $1;`, spaceID)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetExpensesByTag(spaceID string, fromDate, toDate time.Time) ([]*model.TagExpenseSummary, error) {
|
||||
var summaries []*model.TagExpenseSummary
|
||||
query := `
|
||||
SELECT
|
||||
t.id as tag_id,
|
||||
t.name as tag_name,
|
||||
t.color as tag_color,
|
||||
SUM(CAST(e.amount AS DECIMAL)) as total_amount
|
||||
FROM expenses e
|
||||
JOIN expense_tags et ON e.id = et.expense_id
|
||||
JOIN tags t ON et.tag_id = t.id
|
||||
WHERE e.space_id = $1 AND e.type = 'expense' AND e.date >= $2 AND e.date <= $3
|
||||
GROUP BY t.id, t.name, t.color
|
||||
ORDER BY total_amount DESC;
|
||||
`
|
||||
err := r.db.Select(&summaries, query, spaceID, fromDate, toDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetTagsByExpenseIDs(expenseIDs []string) (map[string][]*model.Tag, error) {
|
||||
if len(expenseIDs) == 0 {
|
||||
return make(map[string][]*model.Tag), nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
ExpenseID string `db:"expense_id"`
|
||||
ID string `db:"id"`
|
||||
SpaceID string `db:"space_id"`
|
||||
Name string `db:"name"`
|
||||
Color *string `db:"color"`
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(`
|
||||
SELECT et.expense_id, t.id, t.space_id, t.name, t.color
|
||||
FROM expense_tags et
|
||||
JOIN tags t ON et.tag_id = t.id
|
||||
WHERE et.expense_id IN (?)
|
||||
ORDER BY t.name;
|
||||
`, expenseIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = r.db.Rebind(query)
|
||||
|
||||
var rows []row
|
||||
if err := r.db.Select(&rows, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string][]*model.Tag)
|
||||
for _, rw := range rows {
|
||||
result[rw.ExpenseID] = append(result[rw.ExpenseID], &model.Tag{
|
||||
ID: rw.ID,
|
||||
SpaceID: rw.SpaceID,
|
||||
Name: rw.Name,
|
||||
Color: rw.Color,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetPaymentMethodsByExpenseIDs(expenseIDs []string) (map[string]*model.PaymentMethod, error) {
|
||||
if len(expenseIDs) == 0 {
|
||||
return make(map[string]*model.PaymentMethod), nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
ExpenseID string `db:"expense_id"`
|
||||
ID string `db:"id"`
|
||||
SpaceID string `db:"space_id"`
|
||||
Name string `db:"name"`
|
||||
Type model.PaymentMethodType `db:"type"`
|
||||
LastFour *string `db:"last_four"`
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(`
|
||||
SELECT e.id AS expense_id, pm.id, pm.space_id, pm.name, pm.type, pm.last_four
|
||||
FROM expenses e
|
||||
JOIN payment_methods pm ON e.payment_method_id = pm.id
|
||||
WHERE e.id IN (?) AND e.payment_method_id IS NOT NULL;
|
||||
`, expenseIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = r.db.Rebind(query)
|
||||
|
||||
var rows []row
|
||||
if err := r.db.Select(&rows, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]*model.PaymentMethod)
|
||||
for _, rw := range rows {
|
||||
result[rw.ExpenseID] = &model.PaymentMethod{
|
||||
ID: rw.ID,
|
||||
SpaceID: rw.SpaceID,
|
||||
Name: rw.Name,
|
||||
Type: rw.Type,
|
||||
LastFour: rw.LastFour,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *expenseRepository) Update(expense *model.Expense, tagIDs []string) error {
|
||||
return WithTx(r.db, func(tx *sqlx.Tx) error {
|
||||
query := `UPDATE expenses SET description = $1, amount = $2, type = $3, date = $4, payment_method_id = $5, updated_at = $6 WHERE id = $7;`
|
||||
if _, err := tx.Exec(query, expense.Description, expense.Amount, expense.Type, expense.Date, expense.PaymentMethodID, expense.UpdatedAt, expense.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM expense_tags WHERE expense_id = $1;`, expense.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tagIDs) > 0 {
|
||||
insertTag := `INSERT INTO expense_tags (expense_id, tag_id) VALUES ($1, $2);`
|
||||
for _, tagID := range tagIDs {
|
||||
if _, err := tx.Exec(insertTag, expense.ID, tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *expenseRepository) Delete(id string) error {
|
||||
result, err := r.db.Exec(`DELETE FROM expenses WHERE id = $1;`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrExpenseNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetDailySpending(spaceID string, from, to time.Time) ([]*model.DailySpending, error) {
|
||||
var results []*model.DailySpending
|
||||
query := `
|
||||
SELECT date, SUM(CAST(amount AS DECIMAL)) as total
|
||||
FROM expenses
|
||||
WHERE space_id = $1 AND type = 'expense' AND date >= $2 AND date <= $3
|
||||
GROUP BY date
|
||||
ORDER BY date ASC;
|
||||
`
|
||||
err := r.db.Select(&results, query, spaceID, from, to)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetMonthlySpending(spaceID string, from, to time.Time) ([]*model.MonthlySpending, error) {
|
||||
var results []*model.MonthlySpending
|
||||
var query string
|
||||
if r.db.DriverName() == "sqlite" {
|
||||
query = `
|
||||
SELECT strftime('%Y-%m', date) as month, SUM(CAST(amount AS DECIMAL)) as total
|
||||
FROM expenses
|
||||
WHERE space_id = $1 AND type = 'expense' AND date >= $2 AND date <= $3
|
||||
GROUP BY strftime('%Y-%m', date)
|
||||
ORDER BY month ASC;`
|
||||
} else {
|
||||
query = `
|
||||
SELECT TO_CHAR(date, 'YYYY-MM') as month, SUM(CAST(amount AS DECIMAL)) as total
|
||||
FROM expenses
|
||||
WHERE space_id = $1 AND type = 'expense' AND date >= $2 AND date <= $3
|
||||
GROUP BY TO_CHAR(date, 'YYYY-MM')
|
||||
ORDER BY month ASC;`
|
||||
}
|
||||
err := r.db.Select(&results, query, spaceID, from, to)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetTopExpenses(spaceID string, from, to time.Time, limit int) ([]*model.Expense, error) {
|
||||
var results []*model.Expense
|
||||
query := `
|
||||
SELECT * FROM expenses
|
||||
WHERE space_id = $1 AND type = 'expense' AND date >= $2 AND date <= $3
|
||||
ORDER BY CAST(amount AS DECIMAL) DESC
|
||||
LIMIT $4;
|
||||
`
|
||||
err := r.db.Select(&results, query, spaceID, from, to, limit)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetIncomeVsExpenseSummary(spaceID string, from, to time.Time) (decimal.Decimal, decimal.Decimal, error) {
|
||||
type summary struct {
|
||||
Type string `db:"type"`
|
||||
Total decimal.Decimal `db:"total"`
|
||||
}
|
||||
var results []summary
|
||||
query := `
|
||||
SELECT type, COALESCE(SUM(CAST(amount AS DECIMAL)), 0) as total
|
||||
FROM expenses
|
||||
WHERE space_id = $1 AND date >= $2 AND date <= $3
|
||||
GROUP BY type;
|
||||
`
|
||||
err := r.db.Select(&results, query, spaceID, from, to)
|
||||
if err != nil {
|
||||
return decimal.Zero, decimal.Zero, err
|
||||
}
|
||||
|
||||
income := decimal.Zero
|
||||
expenseTotal := decimal.Zero
|
||||
for _, r := range results {
|
||||
if r.Type == "topup" {
|
||||
income = r.Total
|
||||
} else if r.Type == "expense" {
|
||||
expenseTotal = r.Total
|
||||
}
|
||||
}
|
||||
return income, expenseTotal, nil
|
||||
}
|
||||
|
||||
func (r *expenseRepository) GetExpensesByPaymentMethod(spaceID string, from, to time.Time) ([]*model.PaymentMethodExpenseSummary, error) {
|
||||
var summaries []*model.PaymentMethodExpenseSummary
|
||||
query := `
|
||||
SELECT COALESCE(pm.id, 'cash') as payment_method_id,
|
||||
COALESCE(pm.name, 'Cash') as payment_method_name,
|
||||
COALESCE(pm.type, 'cash') as payment_method_type,
|
||||
SUM(CAST(e.amount AS DECIMAL)) as total_amount
|
||||
FROM expenses e
|
||||
LEFT JOIN payment_methods pm ON e.payment_method_id = pm.id
|
||||
WHERE e.space_id = $1 AND e.type = 'expense' AND e.date >= $2 AND e.date <= $3
|
||||
GROUP BY pm.id, pm.name, pm.type
|
||||
ORDER BY total_amount DESC;
|
||||
`
|
||||
err := r.db.Select(&summaries, query, spaceID, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return summaries, nil
|
||||
}
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExpenseRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Food", nil)
|
||||
|
||||
now := time.Now()
|
||||
expense := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Lunch",
|
||||
Amount: decimal.RequireFromString("15.49"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(expense, []string{tag.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(expense.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expense.ID, fetched.ID)
|
||||
assert.Equal(t, "Lunch", fetched.Description)
|
||||
assert.True(t, decimal.RequireFromString("15.49").Equal(fetched.Amount))
|
||||
assert.Equal(t, model.ExpenseTypeExpense, fetched.Type)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_GetBySpaceIDPaginated(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 1", decimal.RequireFromString("10.75"), model.ExpenseTypeExpense)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 2", decimal.RequireFromString("20.50"), model.ExpenseTypeExpense)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 3", decimal.RequireFromString("30.25"), model.ExpenseTypeExpense)
|
||||
|
||||
expenses, err := repo.GetBySpaceIDPaginated(space.ID, 2, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, expenses, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_CountBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 1", decimal.RequireFromString("10.75"), model.ExpenseTypeExpense)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 2", decimal.RequireFromString("20.50"), model.ExpenseTypeExpense)
|
||||
|
||||
count, err := repo.CountBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_GetTagsByExpenseIDs(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Groceries", nil)
|
||||
|
||||
now := time.Now()
|
||||
expense := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Weekly groceries",
|
||||
Amount: decimal.RequireFromString("49.99"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(expense, []string{tag.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
tagMap, err := repo.GetTagsByExpenseIDs([]string{expense.ID})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, tagMap, expense.ID)
|
||||
require.Len(t, tagMap[expense.ID], 1)
|
||||
assert.Equal(t, tag.ID, tagMap[expense.ID][0].ID)
|
||||
assert.Equal(t, "Groceries", tagMap[expense.ID][0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_GetPaymentMethodsByExpenseIDs(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Visa", model.PaymentMethodTypeCredit, user.ID)
|
||||
|
||||
now := time.Now()
|
||||
expense := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Online purchase",
|
||||
Amount: decimal.RequireFromString("29.95"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
PaymentMethodID: &method.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(expense, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
methodMap, err := repo.GetPaymentMethodsByExpenseIDs([]string{expense.ID})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, methodMap, expense.ID)
|
||||
assert.Equal(t, method.ID, methodMap[expense.ID].ID)
|
||||
assert.Equal(t, "Visa", methodMap[expense.ID].Name)
|
||||
assert.Equal(t, model.PaymentMethodTypeCredit, methodMap[expense.ID].Type)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_GetExpensesByTag(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
color := "#ff0000"
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Food", &color)
|
||||
|
||||
now := time.Now()
|
||||
fromDate := now.Add(-24 * time.Hour)
|
||||
toDate := now.Add(24 * time.Hour)
|
||||
|
||||
expense1 := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Lunch",
|
||||
Amount: decimal.RequireFromString("15.49"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
err := repo.Create(expense1, []string{tag.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expense2 := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Dinner",
|
||||
Amount: decimal.RequireFromString("24.52"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
err = repo.Create(expense2, []string{tag.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
summaries, err := repo.GetExpensesByTag(space.ID, fromDate, toDate)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, summaries, 1)
|
||||
assert.Equal(t, tag.ID, summaries[0].TagID)
|
||||
assert.Equal(t, "Food", summaries[0].TagName)
|
||||
assert.True(t, decimal.RequireFromString("40.01").Equal(summaries[0].TotalAmount))
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
tag1 := testutil.CreateTestTag(t, dbi.DB, space.ID, "Tag A", nil)
|
||||
tag2 := testutil.CreateTestTag(t, dbi.DB, space.ID, "Tag B", nil)
|
||||
|
||||
now := time.Now()
|
||||
expense := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Original",
|
||||
Amount: decimal.RequireFromString("10.75"),
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(expense, []string{tag1.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expense.Description = "Updated"
|
||||
expense.UpdatedAt = time.Now()
|
||||
err = repo.Update(expense, []string{tag2.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(expense.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated", fetched.Description)
|
||||
|
||||
tagMap, err := repo.GetTagsByExpenseIDs([]string{expense.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tagMap[expense.ID], 1)
|
||||
assert.Equal(t, tag2.ID, tagMap[expense.ID][0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
expense := testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "To Delete", decimal.RequireFromString("4.99"), model.ExpenseTypeExpense)
|
||||
|
||||
err := repo.Delete(expense.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(expense.ID)
|
||||
assert.ErrorIs(t, err, ErrExpenseNotFound)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrListItemNotFound = errors.New("list item not found")
|
||||
)
|
||||
|
||||
type ListItemRepository interface {
|
||||
Create(item *model.ListItem) error
|
||||
GetByID(id string) (*model.ListItem, error)
|
||||
GetByListID(listID string) ([]*model.ListItem, error)
|
||||
GetByListIDPaginated(listID string, limit, offset int) ([]*model.ListItem, error)
|
||||
CountByListID(listID string) (int, error)
|
||||
Update(item *model.ListItem) error
|
||||
Delete(id string) error
|
||||
DeleteByListID(listID string) error
|
||||
}
|
||||
|
||||
type listItemRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewListItemRepository(db *sqlx.DB) ListItemRepository {
|
||||
return &listItemRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *listItemRepository) Create(item *model.ListItem) error {
|
||||
query := `INSERT INTO list_items (id, list_id, name, is_checked, created_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7);`
|
||||
_, err := r.db.Exec(query, item.ID, item.ListID, item.Name, item.IsChecked, item.CreatedBy, item.CreatedAt, item.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *listItemRepository) GetByID(id string) (*model.ListItem, error) {
|
||||
item := &model.ListItem{}
|
||||
query := `SELECT * FROM list_items WHERE id = $1;`
|
||||
err := r.db.Get(item, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrListItemNotFound
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (r *listItemRepository) GetByListID(listID string) ([]*model.ListItem, error) {
|
||||
var items []*model.ListItem
|
||||
query := `SELECT * FROM list_items WHERE list_id = $1 ORDER BY created_at ASC;`
|
||||
err := r.db.Select(&items, query, listID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *listItemRepository) GetByListIDPaginated(listID string, limit, offset int) ([]*model.ListItem, error) {
|
||||
var items []*model.ListItem
|
||||
query := `SELECT * FROM list_items WHERE list_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3;`
|
||||
err := r.db.Select(&items, query, listID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *listItemRepository) CountByListID(listID string) (int, error) {
|
||||
var count int
|
||||
query := `SELECT COUNT(*) FROM list_items WHERE list_id = $1;`
|
||||
err := r.db.Get(&count, query, listID)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *listItemRepository) Update(item *model.ListItem) error {
|
||||
item.UpdatedAt = time.Now()
|
||||
query := `UPDATE list_items SET name = $1, is_checked = $2, updated_at = $3 WHERE id = $4;`
|
||||
result, err := r.db.Exec(query, item.Name, item.IsChecked, item.UpdatedAt, item.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrListItemNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *listItemRepository) Delete(id string) error {
|
||||
query := `DELETE FROM list_items WHERE id = $1;`
|
||||
result, err := r.db.Exec(query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrListItemNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *listItemRepository) DeleteByListID(listID string) error {
|
||||
query := `DELETE FROM list_items WHERE list_id = $1;`
|
||||
_, err := r.db.Exec(query, listID)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListItemRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
now := time.Now()
|
||||
item := &model.ListItem{
|
||||
ID: uuid.NewString(),
|
||||
ListID: list.ID,
|
||||
Name: "Apples",
|
||||
IsChecked: false,
|
||||
CreatedBy: user.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(item)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(item.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, item.ID, fetched.ID)
|
||||
assert.Equal(t, list.ID, fetched.ListID)
|
||||
assert.Equal(t, "Apples", fetched.Name)
|
||||
assert.False(t, fetched.IsChecked)
|
||||
assert.Equal(t, user.ID, fetched.CreatedBy)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_GetByListID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
item1 := testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item A", user.ID)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
item2 := testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item B", user.ID)
|
||||
|
||||
items, err := repo.GetByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, items, 2)
|
||||
|
||||
// Ordered by created_at ASC, so item1 should be first.
|
||||
assert.Equal(t, item1.ID, items[0].ID)
|
||||
assert.Equal(t, item2.ID, items[1].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_GetByListIDPaginated(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item A", user.ID)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item B", user.ID)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item C", user.ID)
|
||||
|
||||
items, err := repo.GetByListIDPaginated(list.ID, 2, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items, 2)
|
||||
|
||||
count, err := repo.CountByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_CountByListID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item A", user.ID)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item B", user.ID)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item C", user.ID)
|
||||
|
||||
count, err := repo.CountByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
item := testutil.CreateTestListItem(t, dbi.DB, list.ID, "Original", user.ID)
|
||||
|
||||
item.Name = "Updated"
|
||||
item.IsChecked = true
|
||||
err := repo.Update(item)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(item.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated", fetched.Name)
|
||||
assert.True(t, fetched.IsChecked)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
item := testutil.CreateTestListItem(t, dbi.DB, list.ID, "To Delete", user.ID)
|
||||
|
||||
err := repo.Delete(item.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(item.ID)
|
||||
assert.ErrorIs(t, err, ErrListItemNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_DeleteByListID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item A", user.ID)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item B", user.ID)
|
||||
|
||||
err := repo.DeleteByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err := repo.GetByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, items)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLoanNotFound = errors.New("loan not found")
|
||||
)
|
||||
|
||||
type LoanRepository interface {
|
||||
Create(loan *model.Loan) error
|
||||
GetByID(id string) (*model.Loan, error)
|
||||
GetBySpaceID(spaceID string) ([]*model.Loan, error)
|
||||
GetBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.Loan, error)
|
||||
CountBySpaceID(spaceID string) (int, error)
|
||||
Update(loan *model.Loan) error
|
||||
Delete(id string) error
|
||||
SetPaidOff(id string, paidOff bool) error
|
||||
GetTotalPaidForLoan(loanID string) (decimal.Decimal, error)
|
||||
GetReceiptCountForLoan(loanID string) (int, error)
|
||||
}
|
||||
|
||||
type loanRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewLoanRepository(db *sqlx.DB) LoanRepository {
|
||||
return &loanRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *loanRepository) Create(loan *model.Loan) error {
|
||||
query := `INSERT INTO loans (id, space_id, name, description, original_amount, interest_rate_bps, start_date, end_date, is_paid_off, created_by, created_at, updated_at, original_amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 0);`
|
||||
_, err := r.db.Exec(query, loan.ID, loan.SpaceID, loan.Name, loan.Description, loan.OriginalAmount, loan.InterestRateBps, loan.StartDate, loan.EndDate, loan.IsPaidOff, loan.CreatedBy, loan.CreatedAt, loan.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *loanRepository) GetByID(id string) (*model.Loan, error) {
|
||||
loan := &model.Loan{}
|
||||
query := `SELECT * FROM loans WHERE id = $1;`
|
||||
err := r.db.Get(loan, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrLoanNotFound
|
||||
}
|
||||
return loan, err
|
||||
}
|
||||
|
||||
func (r *loanRepository) GetBySpaceID(spaceID string) ([]*model.Loan, error) {
|
||||
var loans []*model.Loan
|
||||
query := `SELECT * FROM loans WHERE space_id = $1 ORDER BY is_paid_off ASC, created_at DESC;`
|
||||
err := r.db.Select(&loans, query, spaceID)
|
||||
return loans, err
|
||||
}
|
||||
|
||||
func (r *loanRepository) GetBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.Loan, error) {
|
||||
var loans []*model.Loan
|
||||
query := `SELECT * FROM loans WHERE space_id = $1 ORDER BY is_paid_off ASC, created_at DESC LIMIT $2 OFFSET $3;`
|
||||
err := r.db.Select(&loans, query, spaceID, limit, offset)
|
||||
return loans, err
|
||||
}
|
||||
|
||||
func (r *loanRepository) CountBySpaceID(spaceID string) (int, error) {
|
||||
var count int
|
||||
err := r.db.Get(&count, `SELECT COUNT(*) FROM loans WHERE space_id = $1;`, spaceID)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *loanRepository) Update(loan *model.Loan) error {
|
||||
query := `UPDATE loans SET name = $1, description = $2, original_amount = $3, interest_rate_bps = $4, start_date = $5, end_date = $6, updated_at = $7 WHERE id = $8;`
|
||||
result, err := r.db.Exec(query, loan.Name, loan.Description, loan.OriginalAmount, loan.InterestRateBps, loan.StartDate, loan.EndDate, loan.UpdatedAt, loan.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrLoanNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *loanRepository) Delete(id string) error {
|
||||
_, err := r.db.Exec(`DELETE FROM loans WHERE id = $1;`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *loanRepository) SetPaidOff(id string, paidOff bool) error {
|
||||
_, err := r.db.Exec(`UPDATE loans SET is_paid_off = $1, updated_at = $2 WHERE id = $3;`, paidOff, time.Now(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *loanRepository) GetTotalPaidForLoan(loanID string) (decimal.Decimal, error) {
|
||||
var total decimal.Decimal
|
||||
err := r.db.Get(&total, `SELECT COALESCE(SUM(CAST(total_amount AS DECIMAL)), 0) FROM receipts WHERE loan_id = $1;`, loanID)
|
||||
return total, err
|
||||
}
|
||||
|
||||
func (r *loanRepository) GetReceiptCountForLoan(loanID string) (int, error) {
|
||||
var count int
|
||||
err := r.db.Get(&count, `SELECT COUNT(*) FROM receipts WHERE loan_id = $1;`, loanID)
|
||||
return count, err
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMoneyAccountNotFound = errors.New("money account not found")
|
||||
ErrTransferNotFound = errors.New("account transfer not found")
|
||||
)
|
||||
|
||||
type MoneyAccountRepository interface {
|
||||
Create(account *model.MoneyAccount) error
|
||||
GetByID(id string) (*model.MoneyAccount, error)
|
||||
GetBySpaceID(spaceID string) ([]*model.MoneyAccount, error)
|
||||
Update(account *model.MoneyAccount) error
|
||||
Delete(id string) error
|
||||
|
||||
CreateTransfer(transfer *model.AccountTransfer) error
|
||||
GetTransfersByAccountID(accountID string) ([]*model.AccountTransfer, error)
|
||||
DeleteTransfer(id string) error
|
||||
|
||||
GetAccountBalance(accountID string) (decimal.Decimal, error)
|
||||
GetTotalAllocatedForSpace(spaceID string) (decimal.Decimal, error)
|
||||
|
||||
GetTransfersBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.AccountTransferWithAccount, error)
|
||||
CountTransfersBySpaceID(spaceID string) (int, error)
|
||||
}
|
||||
|
||||
type moneyAccountRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewMoneyAccountRepository(db *sqlx.DB) MoneyAccountRepository {
|
||||
return &moneyAccountRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) Create(account *model.MoneyAccount) error {
|
||||
query := `INSERT INTO money_accounts (id, space_id, name, created_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6);`
|
||||
_, err := r.db.Exec(query, account.ID, account.SpaceID, account.Name, account.CreatedBy, account.CreatedAt, account.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) GetByID(id string) (*model.MoneyAccount, error) {
|
||||
account := &model.MoneyAccount{}
|
||||
query := `SELECT * FROM money_accounts WHERE id = $1;`
|
||||
err := r.db.Get(account, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrMoneyAccountNotFound
|
||||
}
|
||||
return account, err
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) GetBySpaceID(spaceID string) ([]*model.MoneyAccount, error) {
|
||||
var accounts []*model.MoneyAccount
|
||||
query := `SELECT * FROM money_accounts WHERE space_id = $1 ORDER BY created_at DESC;`
|
||||
err := r.db.Select(&accounts, query, spaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) Update(account *model.MoneyAccount) error {
|
||||
account.UpdatedAt = time.Now()
|
||||
query := `UPDATE money_accounts SET name = $1, updated_at = $2 WHERE id = $3;`
|
||||
result, err := r.db.Exec(query, account.Name, account.UpdatedAt, account.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrMoneyAccountNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) Delete(id string) error {
|
||||
query := `DELETE FROM money_accounts WHERE id = $1;`
|
||||
result, err := r.db.Exec(query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrMoneyAccountNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) CreateTransfer(transfer *model.AccountTransfer) error {
|
||||
query := `INSERT INTO account_transfers (id, account_id, amount, direction, note, recurring_deposit_id, created_by, created_at, amount_cents) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 0);`
|
||||
_, err := r.db.Exec(query, transfer.ID, transfer.AccountID, transfer.Amount, transfer.Direction, transfer.Note, transfer.RecurringDepositID, transfer.CreatedBy, transfer.CreatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) GetTransfersByAccountID(accountID string) ([]*model.AccountTransfer, error) {
|
||||
var transfers []*model.AccountTransfer
|
||||
query := `SELECT * FROM account_transfers WHERE account_id = $1 ORDER BY created_at DESC;`
|
||||
err := r.db.Select(&transfers, query, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfers, nil
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) DeleteTransfer(id string) error {
|
||||
query := `DELETE FROM account_transfers WHERE id = $1;`
|
||||
result, err := r.db.Exec(query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrTransferNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) GetAccountBalance(accountID string) (decimal.Decimal, error) {
|
||||
var balance decimal.Decimal
|
||||
query := `SELECT COALESCE(SUM(CASE WHEN direction = 'deposit' THEN CAST(amount AS DECIMAL) ELSE -CAST(amount AS DECIMAL) END), 0) FROM account_transfers WHERE account_id = $1;`
|
||||
err := r.db.Get(&balance, query, accountID)
|
||||
return balance, err
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) GetTotalAllocatedForSpace(spaceID string) (decimal.Decimal, error) {
|
||||
var total decimal.Decimal
|
||||
query := `SELECT COALESCE(SUM(CASE WHEN t.direction = 'deposit' THEN CAST(t.amount AS DECIMAL) ELSE -CAST(t.amount AS DECIMAL) END), 0)
|
||||
FROM account_transfers t
|
||||
JOIN money_accounts a ON t.account_id = a.id
|
||||
WHERE a.space_id = $1;`
|
||||
err := r.db.Get(&total, query, spaceID)
|
||||
return total, err
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) GetTransfersBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.AccountTransferWithAccount, error) {
|
||||
var transfers []*model.AccountTransferWithAccount
|
||||
query := `SELECT t.id, t.account_id, t.amount, t.direction, t.note,
|
||||
t.recurring_deposit_id, t.created_by, t.created_at, a.name AS account_name
|
||||
FROM account_transfers t
|
||||
JOIN money_accounts a ON t.account_id = a.id
|
||||
WHERE a.space_id = $1
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT $2 OFFSET $3;`
|
||||
err := r.db.Select(&transfers, query, spaceID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfers, nil
|
||||
}
|
||||
|
||||
func (r *moneyAccountRepository) CountTransfersBySpaceID(spaceID string) (int, error) {
|
||||
var count int
|
||||
query := `SELECT COUNT(*) FROM account_transfers t
|
||||
JOIN money_accounts a ON t.account_id = a.id
|
||||
WHERE a.space_id = $1;`
|
||||
err := r.db.Get(&count, query, spaceID)
|
||||
return count, err
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMoneyAccountRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
now := time.Now()
|
||||
account := &model.MoneyAccount{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Savings",
|
||||
CreatedBy: user.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(account)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, account.ID, fetched.ID)
|
||||
assert.Equal(t, space.ID, fetched.SpaceID)
|
||||
assert.Equal(t, "Savings", fetched.Name)
|
||||
assert.Equal(t, user.ID, fetched.CreatedBy)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_GetBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account A", user.ID)
|
||||
testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account B", user.ID)
|
||||
|
||||
accounts, err := repo.GetBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, accounts, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Original", user.ID)
|
||||
|
||||
account.Name = "Renamed"
|
||||
err := repo.Update(account)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Renamed", fetched.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "To Delete", user.ID)
|
||||
|
||||
err := repo.Delete(account.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(account.ID)
|
||||
assert.ErrorIs(t, err, ErrMoneyAccountNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_CreateTransfer(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Checking", user.ID)
|
||||
|
||||
transfer := &model.AccountTransfer{
|
||||
ID: uuid.NewString(),
|
||||
AccountID: account.ID,
|
||||
Amount: decimal.RequireFromString("49.95"),
|
||||
Direction: model.TransferDirectionDeposit,
|
||||
Note: "Initial deposit",
|
||||
CreatedBy: user.ID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.CreateTransfer(transfer)
|
||||
require.NoError(t, err)
|
||||
|
||||
transfers, err := repo.GetTransfersByAccountID(account.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, transfers, 1)
|
||||
assert.Equal(t, transfer.ID, transfers[0].ID)
|
||||
assert.True(t, decimal.RequireFromString("49.95").Equal(transfers[0].Amount))
|
||||
assert.Equal(t, model.TransferDirectionDeposit, transfers[0].Direction)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_DeleteTransfer(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Checking", user.ID)
|
||||
transfer := testutil.CreateTestTransfer(t, dbi.DB, account.ID, decimal.RequireFromString("10.25"), model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
err := repo.DeleteTransfer(transfer.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
transfers, err := repo.GetTransfersByAccountID(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, transfers)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_GetAccountBalance(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Checking", user.ID)
|
||||
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, decimal.RequireFromString("10.50"), model.TransferDirectionDeposit, user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, decimal.RequireFromString("3.25"), model.TransferDirectionWithdrawal, user.ID)
|
||||
|
||||
balance, err := repo.GetAccountBalance(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, decimal.RequireFromString("7.25").Equal(balance))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_GetTotalAllocatedForSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
account1 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account A", user.ID)
|
||||
account2 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account B", user.ID)
|
||||
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account1.ID, decimal.RequireFromString("20.75"), model.TransferDirectionDeposit, user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account2.ID, decimal.RequireFromString("29.50"), model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
total, err := repo.GetTotalAllocatedForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, decimal.RequireFromString("50.25").Equal(total))
|
||||
})
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPaymentMethodNotFound = errors.New("payment method not found")
|
||||
)
|
||||
|
||||
type PaymentMethodRepository interface {
|
||||
Create(method *model.PaymentMethod) error
|
||||
GetByID(id string) (*model.PaymentMethod, error)
|
||||
GetBySpaceID(spaceID string) ([]*model.PaymentMethod, error)
|
||||
Update(method *model.PaymentMethod) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type paymentMethodRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewPaymentMethodRepository(db *sqlx.DB) PaymentMethodRepository {
|
||||
return &paymentMethodRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *paymentMethodRepository) Create(method *model.PaymentMethod) error {
|
||||
query := `INSERT INTO payment_methods (id, space_id, name, type, last_four, created_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);`
|
||||
_, err := r.db.Exec(query, method.ID, method.SpaceID, method.Name, method.Type, method.LastFour, method.CreatedBy, method.CreatedAt, method.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *paymentMethodRepository) GetByID(id string) (*model.PaymentMethod, error) {
|
||||
method := &model.PaymentMethod{}
|
||||
query := `SELECT * FROM payment_methods WHERE id = $1;`
|
||||
err := r.db.Get(method, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrPaymentMethodNotFound
|
||||
}
|
||||
return method, err
|
||||
}
|
||||
|
||||
func (r *paymentMethodRepository) GetBySpaceID(spaceID string) ([]*model.PaymentMethod, error) {
|
||||
var methods []*model.PaymentMethod
|
||||
query := `SELECT * FROM payment_methods WHERE space_id = $1 ORDER BY created_at DESC;`
|
||||
err := r.db.Select(&methods, query, spaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return methods, nil
|
||||
}
|
||||
|
||||
func (r *paymentMethodRepository) Update(method *model.PaymentMethod) error {
|
||||
method.UpdatedAt = time.Now()
|
||||
query := `UPDATE payment_methods SET name = $1, type = $2, last_four = $3, updated_at = $4 WHERE id = $5;`
|
||||
result, err := r.db.Exec(query, method.Name, method.Type, method.LastFour, method.UpdatedAt, method.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrPaymentMethodNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *paymentMethodRepository) Delete(id string) error {
|
||||
query := `DELETE FROM payment_methods WHERE id = $1;`
|
||||
result, err := r.db.Exec(query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrPaymentMethodNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPaymentMethodRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewPaymentMethodRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
lastFour := "4242"
|
||||
now := time.Now()
|
||||
method := &model.PaymentMethod{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Visa Gold",
|
||||
Type: model.PaymentMethodTypeCredit,
|
||||
LastFour: &lastFour,
|
||||
CreatedBy: user.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(method)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(method.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, method.ID, fetched.ID)
|
||||
assert.Equal(t, space.ID, fetched.SpaceID)
|
||||
assert.Equal(t, "Visa Gold", fetched.Name)
|
||||
assert.Equal(t, model.PaymentMethodTypeCredit, fetched.Type)
|
||||
require.NotNil(t, fetched.LastFour)
|
||||
assert.Equal(t, "4242", *fetched.LastFour)
|
||||
assert.Equal(t, user.ID, fetched.CreatedBy)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodRepository_GetBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewPaymentMethodRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Visa", model.PaymentMethodTypeCredit, user.ID)
|
||||
testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Debit Card", model.PaymentMethodTypeDebit, user.ID)
|
||||
|
||||
methods, err := repo.GetBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, methods, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewPaymentMethodRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Old Card", model.PaymentMethodTypeCredit, user.ID)
|
||||
|
||||
method.Name = "New Card"
|
||||
method.Type = model.PaymentMethodTypeDebit
|
||||
err := repo.Update(method)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(method.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Card", fetched.Name)
|
||||
assert.Equal(t, model.PaymentMethodTypeDebit, fetched.Type)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewPaymentMethodRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "To Delete", model.PaymentMethodTypeCredit, user.ID)
|
||||
|
||||
err := repo.Delete(method.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(method.ID)
|
||||
assert.ErrorIs(t, err, ErrPaymentMethodNotFound)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProfileNotFound = errors.New("profile not found")
|
||||
)
|
||||
|
||||
type ProfileRepository interface {
|
||||
Create(profile *model.Profile) (string, error)
|
||||
ByUserID(userID string) (*model.Profile, error)
|
||||
UpdateName(userID, name string) error
|
||||
UpdateTimezone(userID, timezone string) error
|
||||
}
|
||||
|
||||
type profileRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewProfileRepository(db *sqlx.DB) *profileRepository {
|
||||
return &profileRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *profileRepository) Create(profile *model.Profile) (string, error) {
|
||||
if profile.CreatedAt.IsZero() {
|
||||
profile.CreatedAt = time.Now()
|
||||
}
|
||||
if profile.UpdatedAt.IsZero() {
|
||||
profile.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
_, err := r.db.Exec(`
|
||||
INSERT INTO profiles (id, user_id, name, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, profile.ID, profile.UserID, profile.Name, profile.CreatedAt, profile.UpdatedAt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return profile.ID, nil
|
||||
}
|
||||
|
||||
func (r *profileRepository) ByUserID(userID string) (*model.Profile, error) {
|
||||
var profile model.Profile
|
||||
err := r.db.Get(&profile, `SELECT * FROM profiles WHERE user_id = $1`, userID)
|
||||
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrProfileNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
func (r *profileRepository) UpdateTimezone(userID, timezone string) error {
|
||||
result, err := r.db.Exec(`
|
||||
UPDATE profiles
|
||||
SET timezone = $1, updated_at = $2
|
||||
WHERE user_id = $3
|
||||
`, timezone, time.Now(), userID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("no profile found for user_id: %s", userID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *profileRepository) UpdateName(userID, name string) error {
|
||||
result, err := r.db.Exec(`
|
||||
UPDATE profiles
|
||||
SET name = $1, updated_at = $2
|
||||
WHERE user_id = $3
|
||||
`, name, time.Now(), userID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("no profile found for user_id: %s", userID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProfileRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewProfileRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "profile-create@example.com", nil)
|
||||
|
||||
now := time.Now()
|
||||
profile := &model.Profile{
|
||||
ID: uuid.NewString(),
|
||||
UserID: user.ID,
|
||||
Name: "Test User",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
id, err := repo.Create(profile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, profile.ID, id)
|
||||
|
||||
fetched, err := repo.ByUserID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Test User", fetched.Name)
|
||||
assert.Equal(t, user.ID, fetched.UserID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_UpdateName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewProfileRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "profile-update@example.com", nil)
|
||||
testutil.CreateTestProfile(t, dbi.DB, user.ID, "Old Name")
|
||||
|
||||
err := repo.UpdateName(user.ID, "New Name")
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.ByUserID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Name", fetched.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_NotFound(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewProfileRepository(dbi.DB)
|
||||
|
||||
_, err := repo.ByUserID("nonexistent-id")
|
||||
assert.ErrorIs(t, err, ErrProfileNotFound)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,327 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrReceiptNotFound = errors.New("receipt not found")
|
||||
)
|
||||
|
||||
type ReceiptRepository interface {
|
||||
CreateWithSources(
|
||||
receipt *model.Receipt,
|
||||
sources []model.ReceiptFundingSource,
|
||||
balanceExpense *model.Expense,
|
||||
accountTransfers []*model.AccountTransfer,
|
||||
) error
|
||||
GetByID(id string) (*model.Receipt, error)
|
||||
GetByLoanIDPaginated(loanID string, limit, offset int) ([]*model.Receipt, error)
|
||||
CountByLoanID(loanID string) (int, error)
|
||||
GetBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.Receipt, error)
|
||||
CountBySpaceID(spaceID string) (int, error)
|
||||
GetFundingSourcesByReceiptID(receiptID string) ([]model.ReceiptFundingSource, error)
|
||||
GetFundingSourcesWithAccountsByReceiptIDs(receiptIDs []string) (map[string][]model.ReceiptFundingSourceWithAccount, error)
|
||||
DeleteWithReversal(receiptID string) error
|
||||
UpdateWithSources(
|
||||
receipt *model.Receipt,
|
||||
sources []model.ReceiptFundingSource,
|
||||
balanceExpense *model.Expense,
|
||||
accountTransfers []*model.AccountTransfer,
|
||||
) error
|
||||
}
|
||||
|
||||
type receiptRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewReceiptRepository(db *sqlx.DB) ReceiptRepository {
|
||||
return &receiptRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *receiptRepository) CreateWithSources(
|
||||
receipt *model.Receipt,
|
||||
sources []model.ReceiptFundingSource,
|
||||
balanceExpense *model.Expense,
|
||||
accountTransfers []*model.AccountTransfer,
|
||||
) error {
|
||||
tx, err := r.db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Insert receipt
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO receipts (id, loan_id, space_id, description, total_amount, date, recurring_receipt_id, created_by, created_at, updated_at, total_amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 0);`,
|
||||
receipt.ID, receipt.LoanID, receipt.SpaceID, receipt.Description, receipt.TotalAmount, receipt.Date, receipt.RecurringReceiptID, receipt.CreatedBy, receipt.CreatedAt, receipt.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert balance expense if present
|
||||
if balanceExpense != nil {
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO expenses (id, space_id, created_by, description, amount, type, date, payment_method_id, recurring_expense_id, created_at, updated_at, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 0);`,
|
||||
balanceExpense.ID, balanceExpense.SpaceID, balanceExpense.CreatedBy, balanceExpense.Description, balanceExpense.Amount, balanceExpense.Type, balanceExpense.Date, balanceExpense.PaymentMethodID, balanceExpense.RecurringExpenseID, balanceExpense.CreatedAt, balanceExpense.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Insert account transfers
|
||||
for _, transfer := range accountTransfers {
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO account_transfers (id, account_id, amount, direction, note, recurring_deposit_id, created_by, created_at, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 0);`,
|
||||
transfer.ID, transfer.AccountID, transfer.Amount, transfer.Direction, transfer.Note, transfer.RecurringDepositID, transfer.CreatedBy, transfer.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Insert funding sources
|
||||
for _, src := range sources {
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO receipt_funding_sources (id, receipt_id, source_type, account_id, amount, linked_expense_id, linked_transfer_id, amount_cents)
|
||||
Values ($1, $2, $3, $4, $5, $6, $7, 0);`,
|
||||
src.ID, src.ReceiptID, src.SourceType, src.AccountID, src.Amount, src.LinkedExpenseID, src.LinkedTransferID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *receiptRepository) GetByID(id string) (*model.Receipt, error) {
|
||||
receipt := &model.Receipt{}
|
||||
query := `SELECT * FROM receipts WHERE id = $1;`
|
||||
err := r.db.Get(receipt, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrReceiptNotFound
|
||||
}
|
||||
return receipt, err
|
||||
}
|
||||
|
||||
func (r *receiptRepository) GetByLoanIDPaginated(loanID string, limit, offset int) ([]*model.Receipt, error) {
|
||||
var receipts []*model.Receipt
|
||||
query := `SELECT * FROM receipts WHERE loan_id = $1 ORDER BY date DESC, created_at DESC LIMIT $2 OFFSET $3;`
|
||||
err := r.db.Select(&receipts, query, loanID, limit, offset)
|
||||
return receipts, err
|
||||
}
|
||||
|
||||
func (r *receiptRepository) CountByLoanID(loanID string) (int, error) {
|
||||
var count int
|
||||
err := r.db.Get(&count, `SELECT COUNT(*) FROM receipts WHERE loan_id = $1;`, loanID)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *receiptRepository) GetBySpaceIDPaginated(spaceID string, limit, offset int) ([]*model.Receipt, error) {
|
||||
var receipts []*model.Receipt
|
||||
query := `SELECT * FROM receipts WHERE space_id = $1 ORDER BY date DESC, created_at DESC LIMIT $2 OFFSET $3;`
|
||||
err := r.db.Select(&receipts, query, spaceID, limit, offset)
|
||||
return receipts, err
|
||||
}
|
||||
|
||||
func (r *receiptRepository) CountBySpaceID(spaceID string) (int, error) {
|
||||
var count int
|
||||
err := r.db.Get(&count, `SELECT COUNT(*) FROM receipts WHERE space_id = $1;`, spaceID)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *receiptRepository) GetFundingSourcesByReceiptID(receiptID string) ([]model.ReceiptFundingSource, error) {
|
||||
var sources []model.ReceiptFundingSource
|
||||
query := `SELECT * FROM receipt_funding_sources WHERE receipt_id = $1;`
|
||||
err := r.db.Select(&sources, query, receiptID)
|
||||
return sources, err
|
||||
}
|
||||
|
||||
func (r *receiptRepository) GetFundingSourcesWithAccountsByReceiptIDs(receiptIDs []string) (map[string][]model.ReceiptFundingSourceWithAccount, error) {
|
||||
if len(receiptIDs) == 0 {
|
||||
return make(map[string][]model.ReceiptFundingSourceWithAccount), nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
ID string `db:"id"`
|
||||
ReceiptID string `db:"receipt_id"`
|
||||
SourceType model.FundingSourceType `db:"source_type"`
|
||||
AccountID *string `db:"account_id"`
|
||||
Amount decimal.Decimal `db:"amount"`
|
||||
LinkedExpenseID *string `db:"linked_expense_id"`
|
||||
LinkedTransferID *string `db:"linked_transfer_id"`
|
||||
AccountName *string `db:"account_name"`
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(`
|
||||
SELECT rfs.id, rfs.receipt_id, rfs.source_type, rfs.account_id, rfs.amount,
|
||||
rfs.linked_expense_id, rfs.linked_transfer_id,
|
||||
ma.name AS account_name
|
||||
FROM receipt_funding_sources rfs
|
||||
LEFT JOIN money_accounts ma ON rfs.account_id = ma.id
|
||||
WHERE rfs.receipt_id IN (?)
|
||||
ORDER BY rfs.source_type ASC;
|
||||
`, receiptIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = r.db.Rebind(query)
|
||||
|
||||
var rows []row
|
||||
if err := r.db.Select(&rows, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string][]model.ReceiptFundingSourceWithAccount)
|
||||
for _, rw := range rows {
|
||||
accountName := ""
|
||||
if rw.AccountName != nil {
|
||||
accountName = *rw.AccountName
|
||||
}
|
||||
result[rw.ReceiptID] = append(result[rw.ReceiptID], model.ReceiptFundingSourceWithAccount{
|
||||
ReceiptFundingSource: model.ReceiptFundingSource{
|
||||
ID: rw.ID,
|
||||
ReceiptID: rw.ReceiptID,
|
||||
SourceType: rw.SourceType,
|
||||
AccountID: rw.AccountID,
|
||||
Amount: rw.Amount,
|
||||
LinkedExpenseID: rw.LinkedExpenseID,
|
||||
LinkedTransferID: rw.LinkedTransferID,
|
||||
},
|
||||
AccountName: accountName,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *receiptRepository) DeleteWithReversal(receiptID string) error {
|
||||
tx, err := r.db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Get all funding sources for this receipt
|
||||
var sources []model.ReceiptFundingSource
|
||||
if err := tx.Select(&sources, `SELECT * FROM receipt_funding_sources WHERE receipt_id = $1;`, receiptID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete linked expenses and transfers
|
||||
for _, src := range sources {
|
||||
if src.LinkedExpenseID != nil {
|
||||
if _, err := tx.Exec(`DELETE FROM expenses WHERE id = $1;`, *src.LinkedExpenseID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if src.LinkedTransferID != nil {
|
||||
if _, err := tx.Exec(`DELETE FROM account_transfers WHERE id = $1;`, *src.LinkedTransferID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete funding sources (cascade would handle this, but be explicit)
|
||||
if _, err := tx.Exec(`DELETE FROM receipt_funding_sources WHERE receipt_id = $1;`, receiptID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the receipt
|
||||
if _, err := tx.Exec(`DELETE FROM receipts WHERE id = $1;`, receiptID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *receiptRepository) UpdateWithSources(
|
||||
receipt *model.Receipt,
|
||||
sources []model.ReceiptFundingSource,
|
||||
balanceExpense *model.Expense,
|
||||
accountTransfers []*model.AccountTransfer,
|
||||
) error {
|
||||
tx, err := r.db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Delete old linked records
|
||||
var oldSources []model.ReceiptFundingSource
|
||||
if err := tx.Select(&oldSources, `SELECT * FROM receipt_funding_sources WHERE receipt_id = $1;`, receipt.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, src := range oldSources {
|
||||
if src.LinkedExpenseID != nil {
|
||||
if _, err := tx.Exec(`DELETE FROM expenses WHERE id = $1;`, *src.LinkedExpenseID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if src.LinkedTransferID != nil {
|
||||
if _, err := tx.Exec(`DELETE FROM account_transfers WHERE id = $1;`, *src.LinkedTransferID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM receipt_funding_sources WHERE receipt_id = $1;`, receipt.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update receipt
|
||||
_, err = tx.Exec(
|
||||
`UPDATE receipts SET description = $1, total_amount = $2, date = $3, updated_at = $4 WHERE id = $5;`,
|
||||
receipt.Description, receipt.TotalAmount, receipt.Date, receipt.UpdatedAt, receipt.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert new balance expense
|
||||
if balanceExpense != nil {
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO expenses (id, space_id, created_by, description, amount, type, date, payment_method_id, recurring_expense_id, created_at, updated_at, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 0);`,
|
||||
balanceExpense.ID, balanceExpense.SpaceID, balanceExpense.CreatedBy, balanceExpense.Description, balanceExpense.Amount, balanceExpense.Type, balanceExpense.Date, balanceExpense.PaymentMethodID, balanceExpense.RecurringExpenseID, balanceExpense.CreatedAt, balanceExpense.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new account transfers
|
||||
for _, transfer := range accountTransfers {
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO account_transfers (id, account_id, amount, direction, note, recurring_deposit_id, created_by, created_at, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 0);`,
|
||||
transfer.ID, transfer.AccountID, transfer.Amount, transfer.Direction, transfer.Note, transfer.RecurringDepositID, transfer.CreatedBy, transfer.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new funding sources
|
||||
for _, src := range sources {
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO receipt_funding_sources (id, receipt_id, source_type, account_id, amount, linked_expense_id, linked_transfer_id, amount_cents)
|
||||
Values ($1, $2, $3, $4, $5, $6, $7, 0);`,
|
||||
src.ID, src.ReceiptID, src.SourceType, src.AccountID, src.Amount, src.LinkedExpenseID, src.LinkedTransferID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRecurringExpenseNotFound = errors.New("recurring expense not found")
|
||||
)
|
||||
|
||||
type RecurringExpenseRepository interface {
|
||||
Create(re *model.RecurringExpense, tagIDs []string) error
|
||||
GetByID(id string) (*model.RecurringExpense, error)
|
||||
GetBySpaceID(spaceID string) ([]*model.RecurringExpense, error)
|
||||
GetTagsByRecurringExpenseIDs(ids []string) (map[string][]*model.Tag, error)
|
||||
GetPaymentMethodsByRecurringExpenseIDs(ids []string) (map[string]*model.PaymentMethod, error)
|
||||
Update(re *model.RecurringExpense, tagIDs []string) error
|
||||
Delete(id string) error
|
||||
SetActive(id string, active bool) error
|
||||
GetDueRecurrences(now time.Time) ([]*model.RecurringExpense, error)
|
||||
GetDueRecurrencesForSpace(spaceID string, now time.Time) ([]*model.RecurringExpense, error)
|
||||
UpdateNextOccurrence(id string, next time.Time) error
|
||||
Deactivate(id string) error
|
||||
}
|
||||
|
||||
type recurringExpenseRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewRecurringExpenseRepository(db *sqlx.DB) RecurringExpenseRepository {
|
||||
return &recurringExpenseRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) Create(re *model.RecurringExpense, tagIDs []string) error {
|
||||
return WithTx(r.db, func(tx *sqlx.Tx) error {
|
||||
query := `INSERT INTO recurring_expenses (id, space_id, created_by, description, amount, type, payment_method_id, frequency, start_date, end_date, next_occurrence, is_active, created_at, updated_at, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, 0);`
|
||||
if _, err := tx.Exec(query, re.ID, re.SpaceID, re.CreatedBy, re.Description, re.Amount, re.Type, re.PaymentMethodID, re.Frequency, re.StartDate, re.EndDate, re.NextOccurrence, re.IsActive, re.CreatedAt, re.UpdatedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tagIDs) > 0 {
|
||||
tagQuery := `INSERT INTO recurring_expense_tags (recurring_expense_id, tag_id) VALUES ($1, $2);`
|
||||
for _, tagID := range tagIDs {
|
||||
if _, err := tx.Exec(tagQuery, re.ID, tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) GetByID(id string) (*model.RecurringExpense, error) {
|
||||
re := &model.RecurringExpense{}
|
||||
query := `SELECT * FROM recurring_expenses WHERE id = $1;`
|
||||
err := r.db.Get(re, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrRecurringExpenseNotFound
|
||||
}
|
||||
return re, err
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) GetBySpaceID(spaceID string) ([]*model.RecurringExpense, error) {
|
||||
var results []*model.RecurringExpense
|
||||
query := `SELECT * FROM recurring_expenses WHERE space_id = $1 ORDER BY is_active DESC, next_occurrence ASC;`
|
||||
err := r.db.Select(&results, query, spaceID)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) GetTagsByRecurringExpenseIDs(ids []string) (map[string][]*model.Tag, error) {
|
||||
if len(ids) == 0 {
|
||||
return make(map[string][]*model.Tag), nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
RecurringExpenseID string `db:"recurring_expense_id"`
|
||||
ID string `db:"id"`
|
||||
SpaceID string `db:"space_id"`
|
||||
Name string `db:"name"`
|
||||
Color *string `db:"color"`
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(`
|
||||
SELECT ret.recurring_expense_id, t.id, t.space_id, t.name, t.color
|
||||
FROM recurring_expense_tags ret
|
||||
JOIN tags t ON ret.tag_id = t.id
|
||||
WHERE ret.recurring_expense_id IN (?)
|
||||
ORDER BY t.name;
|
||||
`, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = r.db.Rebind(query)
|
||||
|
||||
var rows []row
|
||||
if err := r.db.Select(&rows, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string][]*model.Tag)
|
||||
for _, rw := range rows {
|
||||
result[rw.RecurringExpenseID] = append(result[rw.RecurringExpenseID], &model.Tag{
|
||||
ID: rw.ID,
|
||||
SpaceID: rw.SpaceID,
|
||||
Name: rw.Name,
|
||||
Color: rw.Color,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) GetPaymentMethodsByRecurringExpenseIDs(ids []string) (map[string]*model.PaymentMethod, error) {
|
||||
if len(ids) == 0 {
|
||||
return make(map[string]*model.PaymentMethod), nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
RecurringExpenseID string `db:"recurring_expense_id"`
|
||||
ID string `db:"id"`
|
||||
SpaceID string `db:"space_id"`
|
||||
Name string `db:"name"`
|
||||
Type model.PaymentMethodType `db:"type"`
|
||||
LastFour *string `db:"last_four"`
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(`
|
||||
SELECT re.id AS recurring_expense_id, pm.id, pm.space_id, pm.name, pm.type, pm.last_four
|
||||
FROM recurring_expenses re
|
||||
JOIN payment_methods pm ON re.payment_method_id = pm.id
|
||||
WHERE re.id IN (?) AND re.payment_method_id IS NOT NULL;
|
||||
`, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = r.db.Rebind(query)
|
||||
|
||||
var rows []row
|
||||
if err := r.db.Select(&rows, query, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]*model.PaymentMethod)
|
||||
for _, rw := range rows {
|
||||
result[rw.RecurringExpenseID] = &model.PaymentMethod{
|
||||
ID: rw.ID,
|
||||
SpaceID: rw.SpaceID,
|
||||
Name: rw.Name,
|
||||
Type: rw.Type,
|
||||
LastFour: rw.LastFour,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) Update(re *model.RecurringExpense, tagIDs []string) error {
|
||||
return WithTx(r.db, func(tx *sqlx.Tx) error {
|
||||
query := `UPDATE recurring_expenses SET description = $1, amount = $2, type = $3, payment_method_id = $4, frequency = $5, start_date = $6, end_date = $7, next_occurrence = $8, updated_at = $9 WHERE id = $10;`
|
||||
if _, err := tx.Exec(query, re.Description, re.Amount, re.Type, re.PaymentMethodID, re.Frequency, re.StartDate, re.EndDate, re.NextOccurrence, re.UpdatedAt, re.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM recurring_expense_tags WHERE recurring_expense_id = $1;`, re.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tagIDs) > 0 {
|
||||
tagQuery := `INSERT INTO recurring_expense_tags (recurring_expense_id, tag_id) VALUES ($1, $2);`
|
||||
for _, tagID := range tagIDs {
|
||||
if _, err := tx.Exec(tagQuery, re.ID, tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) Delete(id string) error {
|
||||
result, err := r.db.Exec(`DELETE FROM recurring_expenses WHERE id = $1;`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrRecurringExpenseNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) SetActive(id string, active bool) error {
|
||||
_, err := r.db.Exec(`UPDATE recurring_expenses SET is_active = $1, updated_at = $2 WHERE id = $3;`, active, time.Now(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) GetDueRecurrences(now time.Time) ([]*model.RecurringExpense, error) {
|
||||
var results []*model.RecurringExpense
|
||||
query := `SELECT * FROM recurring_expenses WHERE is_active = true AND next_occurrence <= $1;`
|
||||
err := r.db.Select(&results, query, now)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) GetDueRecurrencesForSpace(spaceID string, now time.Time) ([]*model.RecurringExpense, error) {
|
||||
var results []*model.RecurringExpense
|
||||
query := `SELECT * FROM recurring_expenses WHERE is_active = true AND space_id = $1 AND next_occurrence <= $2;`
|
||||
err := r.db.Select(&results, query, spaceID, now)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) UpdateNextOccurrence(id string, next time.Time) error {
|
||||
_, err := r.db.Exec(`UPDATE recurring_expenses SET next_occurrence = $1, updated_at = $2 WHERE id = $3;`, next, time.Now(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *recurringExpenseRepository) Deactivate(id string) error {
|
||||
return r.SetActive(id, false)
|
||||
}
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRecurringReceiptNotFound = errors.New("recurring receipt not found")
|
||||
)
|
||||
|
||||
type RecurringReceiptRepository interface {
|
||||
Create(rr *model.RecurringReceipt, sources []model.RecurringReceiptSource) error
|
||||
GetByID(id string) (*model.RecurringReceipt, error)
|
||||
GetByLoanID(loanID string) ([]*model.RecurringReceipt, error)
|
||||
GetBySpaceID(spaceID string) ([]*model.RecurringReceipt, error)
|
||||
GetSourcesByRecurringReceiptID(id string) ([]model.RecurringReceiptSource, error)
|
||||
Update(rr *model.RecurringReceipt, sources []model.RecurringReceiptSource) error
|
||||
Delete(id string) error
|
||||
SetActive(id string, active bool) error
|
||||
Deactivate(id string) error
|
||||
GetDueRecurrences(now time.Time) ([]*model.RecurringReceipt, error)
|
||||
GetDueRecurrencesForSpace(spaceID string, now time.Time) ([]*model.RecurringReceipt, error)
|
||||
UpdateNextOccurrence(id string, next time.Time) error
|
||||
}
|
||||
|
||||
type recurringReceiptRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewRecurringReceiptRepository(db *sqlx.DB) RecurringReceiptRepository {
|
||||
return &recurringReceiptRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) Create(rr *model.RecurringReceipt, sources []model.RecurringReceiptSource) error {
|
||||
tx, err := r.db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO recurring_receipts (id, loan_id, space_id, description, total_amount, frequency, start_date, end_date, next_occurrence, is_active, created_by, created_at, updated_at, total_amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 0);`,
|
||||
rr.ID, rr.LoanID, rr.SpaceID, rr.Description, rr.TotalAmount, rr.Frequency, rr.StartDate, rr.EndDate, rr.NextOccurrence, rr.IsActive, rr.CreatedBy, rr.CreatedAt, rr.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, src := range sources {
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO recurring_receipt_sources (id, recurring_receipt_id, source_type, account_id, amount, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, 0);`,
|
||||
src.ID, src.RecurringReceiptID, src.SourceType, src.AccountID, src.Amount,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) GetByID(id string) (*model.RecurringReceipt, error) {
|
||||
rr := &model.RecurringReceipt{}
|
||||
query := `SELECT * FROM recurring_receipts WHERE id = $1;`
|
||||
err := r.db.Get(rr, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrRecurringReceiptNotFound
|
||||
}
|
||||
return rr, err
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) GetByLoanID(loanID string) ([]*model.RecurringReceipt, error) {
|
||||
var results []*model.RecurringReceipt
|
||||
query := `SELECT * FROM recurring_receipts WHERE loan_id = $1 ORDER BY is_active DESC, next_occurrence ASC;`
|
||||
err := r.db.Select(&results, query, loanID)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) GetBySpaceID(spaceID string) ([]*model.RecurringReceipt, error) {
|
||||
var results []*model.RecurringReceipt
|
||||
query := `SELECT * FROM recurring_receipts WHERE space_id = $1 ORDER BY is_active DESC, next_occurrence ASC;`
|
||||
err := r.db.Select(&results, query, spaceID)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) GetSourcesByRecurringReceiptID(id string) ([]model.RecurringReceiptSource, error) {
|
||||
var sources []model.RecurringReceiptSource
|
||||
query := `SELECT * FROM recurring_receipt_sources WHERE recurring_receipt_id = $1;`
|
||||
err := r.db.Select(&sources, query, id)
|
||||
return sources, err
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) Update(rr *model.RecurringReceipt, sources []model.RecurringReceiptSource) error {
|
||||
tx, err := r.db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.Exec(
|
||||
`UPDATE recurring_receipts SET description = $1, total_amount = $2, frequency = $3, start_date = $4, end_date = $5, next_occurrence = $6, updated_at = $7 WHERE id = $8;`,
|
||||
rr.Description, rr.TotalAmount, rr.Frequency, rr.StartDate, rr.EndDate, rr.NextOccurrence, rr.UpdatedAt, rr.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace sources
|
||||
if _, err := tx.Exec(`DELETE FROM recurring_receipt_sources WHERE recurring_receipt_id = $1;`, rr.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, src := range sources {
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO recurring_receipt_sources (id, recurring_receipt_id, source_type, account_id, amount, amount_cents)
|
||||
VALUES ($1, $2, $3, $4, $5, 0);`,
|
||||
src.ID, src.RecurringReceiptID, src.SourceType, src.AccountID, src.Amount,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) Delete(id string) error {
|
||||
_, err := r.db.Exec(`DELETE FROM recurring_receipts WHERE id = $1;`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) SetActive(id string, active bool) error {
|
||||
_, err := r.db.Exec(`UPDATE recurring_receipts SET is_active = $1, updated_at = $2 WHERE id = $3;`, active, time.Now(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) Deactivate(id string) error {
|
||||
return r.SetActive(id, false)
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) GetDueRecurrences(now time.Time) ([]*model.RecurringReceipt, error) {
|
||||
var results []*model.RecurringReceipt
|
||||
query := `SELECT * FROM recurring_receipts WHERE is_active = true AND next_occurrence <= $1;`
|
||||
err := r.db.Select(&results, query, now)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) GetDueRecurrencesForSpace(spaceID string, now time.Time) ([]*model.RecurringReceipt, error) {
|
||||
var results []*model.RecurringReceipt
|
||||
query := `SELECT * FROM recurring_receipts WHERE is_active = true AND space_id = $1 AND next_occurrence <= $2;`
|
||||
err := r.db.Select(&results, query, spaceID, now)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (r *recurringReceiptRepository) UpdateNextOccurrence(id string, next time.Time) error {
|
||||
_, err := r.db.Exec(`UPDATE recurring_receipts SET next_occurrence = $1, updated_at = $2 WHERE id = $3;`, next, time.Now(), id)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrShoppingListNotFound = errors.New("shopping list not found")
|
||||
)
|
||||
|
||||
type ShoppingListRepository interface {
|
||||
Create(list *model.ShoppingList) error
|
||||
GetByID(id string) (*model.ShoppingList, error)
|
||||
GetBySpaceID(spaceID string) ([]*model.ShoppingList, error)
|
||||
Update(list *model.ShoppingList) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type shoppingListRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewShoppingListRepository(db *sqlx.DB) ShoppingListRepository {
|
||||
return &shoppingListRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *shoppingListRepository) Create(list *model.ShoppingList) error {
|
||||
query := `INSERT INTO shopping_lists (id, space_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5);`
|
||||
_, err := r.db.Exec(query, list.ID, list.SpaceID, list.Name, list.CreatedAt, list.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *shoppingListRepository) GetByID(id string) (*model.ShoppingList, error) {
|
||||
list := &model.ShoppingList{}
|
||||
query := `SELECT * FROM shopping_lists WHERE id = $1;`
|
||||
err := r.db.Get(list, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrShoppingListNotFound
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (r *shoppingListRepository) GetBySpaceID(spaceID string) ([]*model.ShoppingList, error) {
|
||||
var lists []*model.ShoppingList
|
||||
query := `SELECT * FROM shopping_lists WHERE space_id = $1 ORDER BY created_at DESC;`
|
||||
err := r.db.Select(&lists, query, spaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lists, nil
|
||||
}
|
||||
|
||||
func (r *shoppingListRepository) Update(list *model.ShoppingList) error {
|
||||
list.UpdatedAt = time.Now()
|
||||
query := `UPDATE shopping_lists SET name = $1, updated_at = $2 WHERE id = $3;`
|
||||
result, err := r.db.Exec(query, list.Name, list.UpdatedAt, list.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrShoppingListNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *shoppingListRepository) Delete(id string) error {
|
||||
query := `DELETE FROM shopping_lists WHERE id = $1;`
|
||||
result, err := r.db.Exec(query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrShoppingListNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShoppingListRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewShoppingListRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
now := time.Now()
|
||||
list := &model.ShoppingList{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Groceries",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(list)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, list.ID, fetched.ID)
|
||||
assert.Equal(t, space.ID, fetched.SpaceID)
|
||||
assert.Equal(t, "Groceries", fetched.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListRepository_GetBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewShoppingListRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
list1 := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "List A")
|
||||
// Small delay to ensure distinct created_at timestamps for ordering.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
list2 := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "List B")
|
||||
|
||||
lists, err := repo.GetBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, lists, 2)
|
||||
|
||||
// Ordered by created_at DESC, so list2 should be first.
|
||||
assert.Equal(t, list2.ID, lists[0].ID)
|
||||
assert.Equal(t, list1.ID, lists[1].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewShoppingListRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Original Name")
|
||||
|
||||
list.Name = "Updated Name"
|
||||
err := repo.Update(list)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated Name", fetched.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewShoppingListRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "To Delete")
|
||||
|
||||
err := repo.Delete(list.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(list.ID)
|
||||
assert.ErrorIs(t, err, ErrShoppingListNotFound)
|
||||
})
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ type SpaceRepository interface {
|
|||
IsMember(spaceID, userID string) (bool, error)
|
||||
GetMembers(spaceID string) ([]*model.SpaceMemberWithProfile, error)
|
||||
UpdateName(spaceID, name string) error
|
||||
UpdateTimezone(spaceID, timezone string) error
|
||||
|
||||
Delete(spaceID string) error
|
||||
}
|
||||
|
||||
|
|
@ -115,10 +115,9 @@ func (r *spaceRepository) GetMembers(spaceID string) ([]*model.SpaceMemberWithPr
|
|||
var members []*model.SpaceMemberWithProfile
|
||||
query := `
|
||||
SELECT sm.space_id, sm.user_id, sm.role, sm.joined_at,
|
||||
p.name, u.email
|
||||
u.name, u.email
|
||||
FROM space_members sm
|
||||
JOIN users u ON sm.user_id = u.id
|
||||
JOIN profiles p ON sm.user_id = p.user_id
|
||||
WHERE sm.space_id = $1
|
||||
ORDER BY sm.role DESC, sm.joined_at ASC;`
|
||||
err := r.db.Select(&members, query, spaceID)
|
||||
|
|
@ -131,11 +130,6 @@ func (r *spaceRepository) UpdateName(spaceID, name string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (r *spaceRepository) UpdateTimezone(spaceID, timezone string) error {
|
||||
query := `UPDATE spaces SET timezone = $1, updated_at = $2 WHERE id = $3;`
|
||||
_, err := r.db.Exec(query, timezone, time.Now(), spaceID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *spaceRepository) Delete(spaceID string) error {
|
||||
query := `DELETE FROM spaces WHERE id = $1;`
|
||||
|
|
|
|||
|
|
@ -95,8 +95,10 @@ func TestSpaceRepository_GetMembers(t *testing.T) {
|
|||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceRepository(dbi.DB)
|
||||
|
||||
owner, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "members-owner@example.com", "Owner")
|
||||
member, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "members-member@example.com", "Member")
|
||||
ownerName := "Owner"
|
||||
memberName := "Member"
|
||||
owner := testutil.CreateTestUserWithName(t, dbi.DB, "members-owner@example.com", &ownerName)
|
||||
member := testutil.CreateTestUserWithName(t, dbi.DB, "members-member@example.com", &memberName)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Members Space")
|
||||
|
||||
err := repo.AddMember(space.ID, member.ID, model.RoleMember)
|
||||
|
|
@ -108,9 +110,11 @@ func TestSpaceRepository_GetMembers(t *testing.T) {
|
|||
|
||||
// The query orders by role DESC (owner first), then joined_at ASC.
|
||||
assert.Equal(t, model.RoleOwner, members[0].Role)
|
||||
assert.Equal(t, "Owner", members[0].Name)
|
||||
require.NotNil(t, members[0].Name)
|
||||
assert.Equal(t, "Owner", *members[0].Name)
|
||||
assert.Equal(t, model.RoleMember, members[1].Role)
|
||||
assert.Equal(t, "Member", members[1].Name)
|
||||
require.NotNil(t, members[1].Name)
|
||||
assert.Equal(t, "Member", *members[1].Name)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTagNotFound = errors.New("tag not found")
|
||||
ErrDuplicateTagName = errors.New("tag with that name already exists in this space")
|
||||
)
|
||||
|
||||
type TagRepository interface {
|
||||
Create(tag *model.Tag) error
|
||||
GetByID(id string) (*model.Tag, error)
|
||||
GetBySpaceID(spaceID string) ([]*model.Tag, error)
|
||||
Update(tag *model.Tag) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type tagRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewTagRepository(db *sqlx.DB) TagRepository {
|
||||
return &tagRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *tagRepository) Create(tag *model.Tag) error {
|
||||
query := `INSERT INTO tags (id, space_id, name, color, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6);`
|
||||
_, err := r.db.Exec(query, tag.ID, tag.SpaceID, tag.Name, tag.Color, tag.CreatedAt, tag.UpdatedAt)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "UNIQUE constraint failed") || strings.Contains(errStr, "duplicate key value") {
|
||||
return ErrDuplicateTagName
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *tagRepository) GetByID(id string) (*model.Tag, error) {
|
||||
tag := &model.Tag{}
|
||||
query := `SELECT * FROM tags WHERE id = $1;`
|
||||
err := r.db.Get(tag, query, id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrTagNotFound
|
||||
}
|
||||
return tag, err
|
||||
}
|
||||
|
||||
func (r *tagRepository) GetBySpaceID(spaceID string) ([]*model.Tag, error) {
|
||||
var tags []*model.Tag
|
||||
query := `SELECT * FROM tags WHERE space_id = $1 ORDER BY name ASC;`
|
||||
err := r.db.Select(&tags, query, spaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (r *tagRepository) Update(tag *model.Tag) error {
|
||||
tag.UpdatedAt = time.Now()
|
||||
query := `UPDATE tags SET name = $1, color = $2, updated_at = $3 WHERE id = $4;`
|
||||
result, err := r.db.Exec(query, tag.Name, tag.Color, tag.UpdatedAt, tag.ID)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "UNIQUE constraint failed") || strings.Contains(errStr, "duplicate key value") {
|
||||
return ErrDuplicateTagName
|
||||
}
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrTagNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *tagRepository) Delete(id string) error {
|
||||
query := `DELETE FROM tags WHERE id = $1;`
|
||||
result, err := r.db.Exec(query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err == nil && rows == 0 {
|
||||
return ErrTagNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTagRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-create@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Space")
|
||||
|
||||
color := "#ff0000"
|
||||
now := time.Now()
|
||||
tag := &model.Tag{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Groceries",
|
||||
Color: &color,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(tag)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(tag.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Groceries", fetched.Name)
|
||||
assert.Equal(t, &color, fetched.Color)
|
||||
assert.Equal(t, space.ID, fetched.SpaceID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagRepository_GetBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-list@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag List Space")
|
||||
|
||||
// Create tags with names that sort alphabetically: "Alpha" < "Beta".
|
||||
testutil.CreateTestTag(t, dbi.DB, space.ID, "Beta", nil)
|
||||
testutil.CreateTestTag(t, dbi.DB, space.ID, "Alpha", nil)
|
||||
|
||||
tags, err := repo.GetBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tags, 2)
|
||||
assert.Equal(t, "Alpha", tags[0].Name)
|
||||
assert.Equal(t, "Beta", tags[1].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-update@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Update Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Old Tag", nil)
|
||||
|
||||
newColor := "#00ff00"
|
||||
tag.Name = "New Tag"
|
||||
tag.Color = &newColor
|
||||
|
||||
err := repo.Update(tag)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(tag.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Tag", fetched.Name)
|
||||
assert.Equal(t, &newColor, fetched.Color)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-delete@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Delete Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Doomed Tag", nil)
|
||||
|
||||
err := repo.Delete(tag.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(tag.ID)
|
||||
assert.ErrorIs(t, err, ErrTagNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagRepository_DuplicateTagName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-dup@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Dup Space")
|
||||
testutil.CreateTestTag(t, dbi.DB, space.ID, "Duplicate", nil)
|
||||
|
||||
now := time.Now()
|
||||
duplicate := &model.Tag{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Duplicate",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(duplicate)
|
||||
assert.ErrorIs(t, err, ErrDuplicateTagName)
|
||||
})
|
||||
}
|
||||
|
|
@ -31,9 +31,9 @@ func NewUserRepository(db *sqlx.DB) UserRepository {
|
|||
}
|
||||
|
||||
func (r *userRepository) Create(user *model.User) (string, error) {
|
||||
query := `INSERT INTO users (id, email, password_hash, email_verified_at, created_at) VALUES ($1, $2, $3, $4, $5);`
|
||||
query := `INSERT INTO users (id, email, name, password_hash, email_verified_at, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7);`
|
||||
|
||||
_, err := r.db.Exec(query, user.ID, user.Email, user.PasswordHash, user.EmailVerifiedAt, user.CreatedAt)
|
||||
_, err := r.db.Exec(query, user.ID, user.Email, user.Name, user.PasswordHash, user.EmailVerifiedAt, user.CreatedAt, user.UpdatedAt)
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "UNIQUE constraint failed") || strings.Contains(errStr, "duplicate key value") {
|
||||
|
|
@ -70,9 +70,9 @@ func (r *userRepository) ByEmail(email string) (*model.User, error) {
|
|||
}
|
||||
|
||||
func (r *userRepository) Update(user *model.User) error {
|
||||
query := `UPDATE users SET email = $1, password_hash = $2, pending_email = $3, email_verified_at = $4 WHERE id = $5;`
|
||||
query := `UPDATE users SET email = $1, name = $2, password_hash = $3, pending_email = $4, email_verified_at = $5, updated_at = $6 WHERE id = $7;`
|
||||
|
||||
_, err := r.db.Exec(query, user.Email, user.PasswordHash, user.PendingEmail, user.EmailVerifiedAt, user.ID)
|
||||
_, err := r.db.Exec(query, user.Email, user.Name, user.PasswordHash, user.PendingEmail, user.EmailVerifiedAt, user.UpdatedAt, user.ID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue