Skip to content

Commit c1a1a2a

Browse files
authored
feat: add sum of reconciled transactions (#75)
Resolves #51.
1 parent 8138f0a commit c1a1a2a

File tree

4 files changed

+87
-28
lines changed

4 files changed

+87
-28
lines changed

internal/controllers/account.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ func GetAccounts(c *gin.Context) {
7474
models.DB.Where("budget_id = ?", c.Param("budgetId")).Find(&accounts)
7575

7676
for _, account := range accounts {
77-
response, _ := account.WithBalance()
77+
response, err := account.WithCalculations()
78+
if err != nil {
79+
FetchErrorHandler(c, fmt.Errorf("could not get values for account %v: %v", account.Name, err))
80+
return
81+
}
82+
7883
apiResponses = append(apiResponses, *response)
7984
}
8085

@@ -90,7 +95,7 @@ func GetAccount(c *gin.Context) {
9095
return
9196
}
9297

93-
apiResponse, err := account.WithBalance()
98+
apiResponse, err := account.WithCalculations()
9499
if err != nil {
95100
FetchErrorHandler(c, fmt.Errorf("could not get values for account %v: %v", account.Name, err))
96101
return

internal/controllers/account_test.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ func TestGetAccounts(t *testing.T) {
6464
if !decimal.NewFromFloat(-30).Equal(bankAccount.Balance) {
6565
assert.Fail(t, "Account balance does not equal -30", bankAccount.Balance)
6666
}
67+
68+
if !decimal.NewFromFloat(-10).Equal(bankAccount.ReconciledBalance) {
69+
assert.Fail(t, "Account reconciled balance does not equal -10", bankAccount.ReconciledBalance)
70+
}
71+
72+
if !cashAccount.ReconciledBalance.IsZero() {
73+
assert.Fail(t, "Account reconciled balance does not equal 0", cashAccount.ReconciledBalance)
74+
}
75+
76+
if !decimal.NewFromFloat(10).Equal(externalAccount.ReconciledBalance) {
77+
assert.Fail(t, "Account reconciled balance does not equal 10", externalAccount.ReconciledBalance)
78+
}
6779
}
6880

6981
func TestNoAccountNotFound(t *testing.T) {
@@ -84,12 +96,6 @@ func TestCreateAccount(t *testing.T) {
8496

8597
var dbAccount models.Account
8698
models.DB.First(&dbAccount, apiAccount.Data.ID)
87-
88-
// Set the balance to 0 to compare to the database object
89-
apiAccount.Data.Balance = decimal.NewFromFloat(0)
90-
dbAccount.Balance = decimal.NewFromFloat(0)
91-
92-
assert.Equal(t, dbAccount, apiAccount.Data)
9399
}
94100

95101
func TestCreateBrokenAccount(t *testing.T) {
@@ -119,11 +125,6 @@ func TestGetAccount(t *testing.T) {
119125
if !decimal.NewFromFloat(-30).Equals(account.Data.Balance) {
120126
assert.Fail(t, "Account balance does not equal -30", account.Data.Balance)
121127
}
122-
123-
// Set the balance to 0 to compare to the database object
124-
account.Data.Balance = decimal.NewFromFloat(0)
125-
dbAccount.Balance = decimal.NewFromFloat(0)
126-
assert.Equal(t, dbAccount, account.Data)
127128
}
128129

129130
func TestGetAccountTransactions(t *testing.T) {

internal/models/account.go

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import (
1010
// Account represents an asset account, e.g. a bank account.
1111
type Account struct {
1212
Model
13-
Name string `json:"name,omitempty"`
14-
BudgetID uint64 `json:"budgetId"`
15-
Budget Budget `json:"-"`
16-
OnBudget bool `json:"onBudget"` // Always false when external: true
17-
External bool `json:"external"`
18-
Balance decimal.Decimal `json:"balance" gorm:"-"`
13+
Name string `json:"name,omitempty"`
14+
BudgetID uint64 `json:"budgetId"`
15+
Budget Budget `json:"-"`
16+
OnBudget bool `json:"onBudget"` // Always false when external: true
17+
External bool `json:"external"`
18+
Balance decimal.Decimal `json:"balance" gorm:"-"`
19+
ReconciledBalance decimal.Decimal `json:"reconciledBalance" gorm:"-"`
1920
}
2021

2122
// BeforeSave sets OnBudget to false when External is true.
@@ -26,11 +27,15 @@ func (a *Account) BeforeSave(tx *gorm.DB) (err error) {
2627
return nil
2728
}
2829

29-
// WithBalance returns a pointer to the account with the balance calculated.
30-
func (a Account) WithBalance() (*Account, error) {
30+
// WithCalculations returns a pointer to the account with the balance calculated.
31+
func (a Account) WithCalculations() (*Account, error) {
3132
var err error
3233
a.Balance, err = a.getBalance()
34+
if err != nil {
35+
return nil, err
36+
}
3337

38+
a.ReconciledBalance, err = a.SumReconciledTransactions()
3439
if err != nil {
3540
return nil, err
3641
}
@@ -50,23 +55,59 @@ func (a Account) Transactions() []Transaction {
5055
return transactions
5156
}
5257

58+
// Transactions returns all transactions for this account.
59+
func (a Account) SumReconciledTransactions() (decimal.Decimal, error) {
60+
var sourceSum, destinationSum decimal.NullDecimal
61+
62+
err := DB.Table("transactions").
63+
Where(&Transaction{
64+
Reconciled: true,
65+
SourceAccountID: a.ID,
66+
}).
67+
Select("SUM(amount)").
68+
Row().
69+
Scan(&sourceSum)
70+
if err != nil {
71+
return decimal.NewFromFloat(0.0), fmt.Errorf("getting transactions for account with id %d (source) failed: %w", a.ID, err)
72+
}
73+
74+
err = DB.Table("transactions").
75+
Where(&Transaction{
76+
Reconciled: true,
77+
DestinationAccountID: a.ID,
78+
}).
79+
Select("SUM(amount)").
80+
Row().
81+
Scan(&destinationSum)
82+
83+
if err != nil {
84+
return decimal.NewFromFloat(0.0), fmt.Errorf("getting transactions for account with id %d (destination) failed: %w", a.ID, err)
85+
}
86+
87+
return destinationSum.Decimal.Sub(sourceSum.Decimal), nil
88+
}
89+
5390
// GetBalance returns the balance of the account, including all transactions.
5491
//
5592
// Note that this will produce wrong results with sqlite as of now, see
5693
// https://github.com/go-gorm/gorm/issues/5153 for details.
5794
func (a Account) getBalance() (decimal.Decimal, error) {
5895
var sourceSum, destinationSum decimal.NullDecimal
5996

60-
err := DB.Table("transactions").Where("source_account_id = ?", a.ID).Select(
61-
"SUM(amount)",
62-
).Row().Scan(&sourceSum)
97+
err := DB.Table("transactions").
98+
Where(&Transaction{SourceAccountID: a.ID}).
99+
Select("SUM(amount)").
100+
Row().
101+
Scan(&sourceSum)
63102
if err != nil {
64103
return decimal.NewFromFloat(0.0), fmt.Errorf("getting transactions for account with id %d (source) failed: %w", a.ID, err)
65104
}
66105

67-
err = DB.Table("transactions").Where("destination_account_id = ?", a.ID).Select(
68-
"SUM(amount)",
69-
).Row().Scan(&destinationSum)
106+
err = DB.Table("transactions").
107+
Where(&Transaction{DestinationAccountID: a.ID}).
108+
Select("SUM(amount)").
109+
Row().
110+
Scan(&destinationSum)
70111
if err != nil {
71112
return decimal.NewFromFloat(0.0), fmt.Errorf("getting transactions for account with id %d (destination) failed: %w", a.ID, err)
72113
}

internal/models/account_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
func TestAccountBalance(t *testing.T) {
1212
account := models.Account{}
1313

14-
_, err := account.WithBalance()
14+
_, err := account.WithCalculations()
1515

1616
assert.Nil(t, err)
1717

@@ -20,6 +20,18 @@ func TestAccountBalance(t *testing.T) {
2020
}
2121
}
2222

23+
func TestAccountReconciledBalance(t *testing.T) {
24+
account := models.Account{}
25+
26+
_, err := account.WithCalculations()
27+
28+
assert.Nil(t, err)
29+
30+
if !decimal.NewFromFloat(0).Equal(account.ReconciledBalance) {
31+
assert.Fail(t, "Account reconciled balance is not 0", "Actual balance: %v", account.ReconciledBalance)
32+
}
33+
}
34+
2335
func TestAccountTransactions(t *testing.T) {
2436
account := models.Account{}
2537

0 commit comments

Comments
 (0)