From cef62574bb3b4e2abdddf51ecda7c232e23e0789 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 5 Apr 2022 15:08:33 +0000 Subject: [PATCH 01/28] Add assigned, deassigned and activity to budgeting screen --- web/src/pages/Budgeting.vue | 18 +++++++++++++++++- web/src/stores/budget-account.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/web/src/pages/Budgeting.vue b/web/src/pages/Budgeting.vue index 5722854..d800a75 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -79,7 +79,23 @@ function assignedChanged(e : Event, category : Category){ Available balance: + /> +
+ Budgeted this month: + +
+ Budgeted this month: + +
+ Budgeted this month: + +
{ + const IncomeCategoryID = this.GetIncomeCategoryID; + if (IncomeCategoryID == null) return 0; + + const categories = this.AllCategoriesForMonth(year, month); + + let assigned = 0, deassigned = 0; + let activity = 0; + for (const category of categories) { + if (category.ID == IncomeCategoryID) + continue; + + activity += category.Activity; + if(category.Assigned > 0) + assigned += category.Assigned; + else + deassigned += category.Assigned; + } + return { + assigned, + deassigned, + activity + }; + }; + }, GetIncomeAvailable(state) { return (year: number, month: number) => { const IncomeCategoryID = this.GetIncomeCategoryID; From 01c407d4f1ecb26c520343c4f166d7ab878a670b Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 6 Apr 2022 20:29:56 +0000 Subject: [PATCH 02/28] Remove income category from result --- postgres/numeric/numeric.go | 7 +++++++ server/budgeting.go | 18 +++++++----------- web/src/stores/budget-account.ts | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/postgres/numeric/numeric.go b/postgres/numeric/numeric.go index 4988f9e..7405489 100644 --- a/postgres/numeric/numeric.go +++ b/postgres/numeric/numeric.go @@ -25,6 +25,13 @@ func FromInt64WithExp(value int64, exp int32) Numeric { return Numeric{Numeric: pgtype.Numeric{Int: big.NewInt(value), Exp: exp, Status: pgtype.Present}} } +func (n *Numeric) SetZero() { + n.Exp = 0 + n.Int = big.NewInt(0) + n.Status = pgtype.Present + n.NaN = false +} + func (n Numeric) GetFloat64() float64 { if n.Status != pgtype.Present { return 0 diff --git a/server/budgeting.go b/server/budgeting.go index 814d94b..8d3ef05 100644 --- a/server/budgeting.go +++ b/server/budgeting.go @@ -79,7 +79,7 @@ func (h *Handler) prepareBudgeting(ctx context.Context, budget postgres.Budget, return BudgetingForMonthResponse{}, fmt.Errorf("error loading balances: %w", err) } - categoriesWithBalance, moneyUsed := h.calculateBalances(firstOfNextMonth, firstOfMonth, categories, cumultativeBalances) + categoriesWithBalance, moneyUsed := h.calculateBalances(budget, firstOfNextMonth, firstOfMonth, categories, cumultativeBalances) availableBalance := h.getAvailableBalance(budget, moneyUsed, cumultativeBalances, categoriesWithBalance, firstOfNextMonth) data := BudgetingForMonthResponse{categoriesWithBalance, availableBalance} @@ -107,17 +107,9 @@ func (*Handler) getAvailableBalance(budget postgres.Budget, } availableBalance.AddI(bal.Transactions) - availableBalance.AddI(bal.Assignments) + availableBalance.AddI(bal.Assignments) // should be zero, but who knows } - for i := range categoriesWithBalance { - cat := &categoriesWithBalance[i] - if cat.ID != budget.IncomeCategoryID { - continue - } - - cat.Available = availableBalance - } return availableBalance } @@ -155,7 +147,7 @@ func (h *Handler) getBudget(c *gin.Context, budgetUUID uuid.UUID) { c.JSON(http.StatusOK, data) } -func (h *Handler) calculateBalances(firstOfNextMonth time.Time, firstOfMonth time.Time, +func (h *Handler) calculateBalances(budget postgres.Budget, firstOfNextMonth time.Time, firstOfMonth time.Time, categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow, ) ([]CategoryWithBalance, numeric.Numeric) { categoriesWithBalance := []CategoryWithBalance{} @@ -163,6 +155,10 @@ func (h *Handler) calculateBalances(firstOfNextMonth time.Time, firstOfMonth tim moneyUsed := numeric.Zero() for i := range categories { cat := &categories[i] + if cat.ID == budget.IncomeCategoryID { + continue + } + categoryWithBalance := NewCategoryWithBalance(cat) for _, bal := range cumultativeBalances { if bal.CategoryID != cat.ID { diff --git a/web/src/stores/budget-account.ts b/web/src/stores/budget-account.ts index 811dc4c..97d6dd3 100644 --- a/web/src/stores/budget-account.ts +++ b/web/src/stores/budget-account.ts @@ -113,7 +113,7 @@ export const useAccountStore = defineStore("budget/account", { const categoryGroups = []; let prev = undefined; for (const category of categories) { - if (category.ID == this.GetIncomeCategoryID) continue; + //if (category.ID == this.GetIncomeCategoryID) continue; if (prev == undefined || category.Group != prev.Name) { prev = { From 54e591bc5e7d46665a1db0be6aa0ff30a0a90c33 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 6 Apr 2022 21:41:55 +0000 Subject: [PATCH 03/28] Extract Month class --- server/budgeting.go | 36 +++++----------- server/category.go | 2 +- server/main_test.go | 9 ++-- server/util.go | 83 +++++++++++++++++++++++++++++++++++-- web/src/pages/Budgeting.vue | 4 +- 5 files changed, 97 insertions(+), 37 deletions(-) diff --git a/server/budgeting.go b/server/budgeting.go index 8d3ef05..5d2e97e 100644 --- a/server/budgeting.go +++ b/server/budgeting.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "time" "git.javil.eu/jacob1123/budgeteer/postgres" "git.javil.eu/jacob1123/budgeteer/postgres/numeric" @@ -12,17 +11,6 @@ import ( "github.com/google/uuid" ) -func getFirstOfMonth(year, month int, location *time.Location) time.Time { - return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, location) -} - -func getFirstOfMonthTime(date time.Time) time.Time { - var monthM time.Month - year, monthM, _ := date.Date() - month := int(monthM) - return getFirstOfMonth(year, month, date.Location()) -} - type CategoryWithBalance struct { *postgres.GetCategoriesRow Available numeric.Numeric @@ -53,13 +41,13 @@ func (h *Handler) budgetingForMonth(c *gin.Context) { return } - firstOfMonth, err := getDate(c) + month, err := getDate(c) if err != nil { c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budget.ID.String()) return } - data, err := h.prepareBudgeting(c.Request.Context(), budget, firstOfMonth) + data, err := h.getBudgetingViewForMonth(c.Request.Context(), budget, month) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return @@ -67,8 +55,7 @@ func (h *Handler) budgetingForMonth(c *gin.Context) { c.JSON(http.StatusOK, data) } -func (h *Handler) prepareBudgeting(ctx context.Context, budget postgres.Budget, firstOfMonth time.Time) (BudgetingForMonthResponse, error) { - firstOfNextMonth := firstOfMonth.AddDate(0, 1, 0) +func (h *Handler) getBudgetingViewForMonth(ctx context.Context, budget postgres.Budget, month Month) (BudgetingForMonthResponse, error) { categories, err := h.Service.GetCategories(ctx, budget.ID) if err != nil { return BudgetingForMonthResponse{}, fmt.Errorf("error loading categories: %w", err) @@ -79,8 +66,8 @@ func (h *Handler) prepareBudgeting(ctx context.Context, budget postgres.Budget, return BudgetingForMonthResponse{}, fmt.Errorf("error loading balances: %w", err) } - categoriesWithBalance, moneyUsed := h.calculateBalances(budget, firstOfNextMonth, firstOfMonth, categories, cumultativeBalances) - availableBalance := h.getAvailableBalance(budget, moneyUsed, cumultativeBalances, categoriesWithBalance, firstOfNextMonth) + categoriesWithBalance, moneyUsed := h.calculateBalances(budget, month, categories, cumultativeBalances) + availableBalance := h.getAvailableBalance(budget, month, moneyUsed, cumultativeBalances) data := BudgetingForMonthResponse{categoriesWithBalance, availableBalance} return data, nil @@ -91,9 +78,8 @@ type BudgetingForMonthResponse struct { AvailableBalance numeric.Numeric } -func (*Handler) getAvailableBalance(budget postgres.Budget, +func (*Handler) getAvailableBalance(budget postgres.Budget, month Month, moneyUsed numeric.Numeric, cumultativeBalances []postgres.GetCumultativeBalancesRow, - categoriesWithBalance []CategoryWithBalance, firstOfNextMonth time.Time, ) numeric.Numeric { availableBalance := moneyUsed @@ -102,7 +88,7 @@ func (*Handler) getAvailableBalance(budget postgres.Budget, continue } - if !bal.Date.Before(firstOfNextMonth) { + if !month.nextMonth().isAfter(bal.Date) { continue } @@ -147,7 +133,7 @@ func (h *Handler) getBudget(c *gin.Context, budgetUUID uuid.UUID) { c.JSON(http.StatusOK, data) } -func (h *Handler) calculateBalances(budget postgres.Budget, firstOfNextMonth time.Time, firstOfMonth time.Time, +func (h *Handler) calculateBalances(budget postgres.Budget, month Month, categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow, ) ([]CategoryWithBalance, numeric.Numeric) { categoriesWithBalance := []CategoryWithBalance{} @@ -166,19 +152,19 @@ func (h *Handler) calculateBalances(budget postgres.Budget, firstOfNextMonth tim } // skip everything in the future - if !bal.Date.Before(firstOfNextMonth) { + if month.nextMonth().isAfter(bal.Date) { continue } moneyUsed.SubI(bal.Assignments) categoryWithBalance.Available.AddI(bal.Assignments) categoryWithBalance.Available.AddI(bal.Transactions) - if !categoryWithBalance.Available.IsPositive() && bal.Date.Before(firstOfMonth) { + if !categoryWithBalance.Available.IsPositive() && month.isAfter(bal.Date) { moneyUsed.AddI(categoryWithBalance.Available) categoryWithBalance.Available = numeric.Zero() } - if bal.Date.Year() == firstOfMonth.Year() && bal.Date.Month() == firstOfMonth.Month() { + if month.contains(bal.Date) { categoryWithBalance.Activity = bal.Transactions categoryWithBalance.Assigned = bal.Assignments } diff --git a/server/category.go b/server/category.go index ab4676c..1881f2a 100644 --- a/server/category.go +++ b/server/category.go @@ -44,7 +44,7 @@ func (h *Handler) setCategoryAssignment(c *gin.Context) { updateArgs := postgres.UpdateAssignmentParams{ CategoryID: categoryUUID, - Date: date, + Date: date.FirstOfMonth(), Amount: amount, } err = h.Service.UpdateAssignment(c.Request.Context(), updateArgs) diff --git a/server/main_test.go b/server/main_test.go index 674402f..99fa6c6 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -181,9 +181,8 @@ func AssertCategoriesAndAvailableEqual(ctx context.Context, t *testing.T, loc *t } year, _ := strconv.Atoi(parts[0]) month, _ := strconv.Atoi(parts[1]) - first := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, loc) testCaseFile := filepath.Join(resultDir, file.Name()) - handler.CheckAvailableBalance(ctx, t, testCaseFile, budget, first) + handler.CheckAvailableBalance(ctx, t, testCaseFile, budget, Month{year, month}) } }) } @@ -199,12 +198,12 @@ type CategoryTestData struct { Assigned float64 } -func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCaseFile string, budget *postgres.Budget, first time.Time) { +func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCaseFile string, budget *postgres.Budget, month Month) { t.Helper() - t.Run(first.Format("2006-01"), func(t *testing.T) { + t.Run(month.String(), func(t *testing.T) { t.Parallel() - data, err := h.prepareBudgeting(ctx, *budget, first) + data, err := h.getBudgetingViewForMonth(ctx, *budget, month) if err != nil { t.Errorf("prepare budgeting: %s", err) return diff --git a/server/util.go b/server/util.go index ffb63a4..531a88e 100644 --- a/server/util.go +++ b/server/util.go @@ -8,7 +8,75 @@ import ( "github.com/gin-gonic/gin" ) -func getDate(c *gin.Context) (time.Time, error) { +type Month struct { + Year int + Month int +} + +func NewFromTime(date time.Time) Month { + return Month{date.Year(), int(date.Month())} +} + +func (m Month) String() string { + return fmt.Sprintf("%d-%d", m.Year, m.Month) +} + +func (m Month) FirstOfMonth() time.Time { + return time.Date(m.Year, time.Month(m.Month), 1, 0, 0, 0, 0, time.Now().Location()) +} + +func (m Month) nextMonth() Month { + if m.Month == 12 { + return Month{m.Year + 1, 1} + } + return Month{m.Year, m.Month} +} + +func (m Month) InFuture(date time.Time) bool { + if m.Year < date.Year() { + return true + } + + if m.Year > date.Year() { + return false + } + + return time.Month(m.Month) < date.Month() +} + +func (m Month) isBeforeOrContains(date time.Time) bool { + if m.Year < date.Year() { + return true + } + + if m.Year > date.Year() { + return false + } + + return time.Month(m.Month) <= date.Month() +} + +func (m Month) InPast(date time.Time) bool { + if m.Year > date.Year() { + return false + } + + if m.Year < date.Year() { + return true + } + + return time.Month(m.Month) > date.Month() +} + +func (m Month) InPresent(date time.Time) bool { + if date.Year() != m.Year { + return false + } + + return date.Month() == time.Month(m.Month) +} + +func getDate(c *gin.Context) (Month, error) { var year, month int yearString := c.Param("year") monthString := c.Param("month") @@ -18,13 +86,20 @@ func getDate(c *gin.Context) (time.Time, error) { year, err := strconv.Atoi(yearString) if err != nil { - return time.Time{}, fmt.Errorf("parse year: %w", err) + return Month{}, fmt.Errorf("parse year: %w", err) } month, err = strconv.Atoi(monthString) if err != nil { - return time.Time{}, fmt.Errorf("parse month: %w", err) + return Month{}, fmt.Errorf("parse month: %w", err) } - return getFirstOfMonth(year, month, time.Now().Location()), nil + return Month{year, month}, nil +} + +func getFirstOfMonthTime(date time.Time) Month { + var monthM time.Month + year, monthM, _ := date.Date() + month := int(monthM) + return Month{year, month} } diff --git a/web/src/pages/Budgeting.vue b/web/src/pages/Budgeting.vue index d800a75..8b4dd8a 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -86,12 +86,12 @@ function assignedChanged(e : Event, category : Category){ :value="accountStore.GetBudgeted(selected.Year, selected.Month).assigned" />
- Budgeted this month: + Deassigned this month:
- Budgeted this month: + Spent this month: From 35690681cfdeda2f960634b96de326e1d2998ded Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 6 Apr 2022 21:44:43 +0000 Subject: [PATCH 04/28] Fix calls to isAfter and so on --- server/budgeting.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/budgeting.go b/server/budgeting.go index 5d2e97e..1424f8d 100644 --- a/server/budgeting.go +++ b/server/budgeting.go @@ -88,7 +88,7 @@ func (*Handler) getAvailableBalance(budget postgres.Budget, month Month, continue } - if !month.nextMonth().isAfter(bal.Date) { + if !month.InPast(bal.Date) { continue } @@ -152,19 +152,19 @@ func (h *Handler) calculateBalances(budget postgres.Budget, month Month, } // skip everything in the future - if month.nextMonth().isAfter(bal.Date) { + if month.InFuture(bal.Date) { continue } moneyUsed.SubI(bal.Assignments) categoryWithBalance.Available.AddI(bal.Assignments) categoryWithBalance.Available.AddI(bal.Transactions) - if !categoryWithBalance.Available.IsPositive() && month.isAfter(bal.Date) { + if !categoryWithBalance.Available.IsPositive() && month.InPast(bal.Date) { moneyUsed.AddI(categoryWithBalance.Available) categoryWithBalance.Available = numeric.Zero() } - if month.contains(bal.Date) { + if month.InPresent(bal.Date) { categoryWithBalance.Activity = bal.Transactions categoryWithBalance.Assigned = bal.Assignments } From 8fe91293e1841a43e37a3e73cf61c6ebcd616b68 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 6 Apr 2022 22:16:50 +0000 Subject: [PATCH 05/28] Add unittests for month --- server/budgeting.go | 2 +- server/main_test.go | 9 ++++++ server/month.go | 55 +++++++++++++++++++++++++++++++++++ server/month_test.go | 46 ++++++++++++++++++++++++++++++ server/util.go | 68 -------------------------------------------- 5 files changed, 111 insertions(+), 69 deletions(-) create mode 100644 server/month.go create mode 100644 server/month_test.go diff --git a/server/budgeting.go b/server/budgeting.go index 1424f8d..5960a87 100644 --- a/server/budgeting.go +++ b/server/budgeting.go @@ -88,7 +88,7 @@ func (*Handler) getAvailableBalance(budget postgres.Budget, month Month, continue } - if !month.InPast(bal.Date) { + if month.InFuture(bal.Date) { continue } diff --git a/server/main_test.go b/server/main_test.go index 99fa6c6..00b31fc 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -245,6 +245,15 @@ func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCa }) } +func AssertEqualBool(t *testing.T, expected, actual bool, message string) { + t.Helper() + if expected == actual { + return + } + + t.Errorf("%s: expected %v, got %v", message, expected, actual) +} + func assertEqual(t *testing.T, expected, actual float64, message string) { t.Helper() if expected == actual { diff --git a/server/month.go b/server/month.go new file mode 100644 index 0000000..4d46f3e --- /dev/null +++ b/server/month.go @@ -0,0 +1,55 @@ +package server + +import ( + "fmt" + "time" +) + +type Month struct { + Year int + Month int +} + +func NewFromTime(date time.Time) Month { + return Month{date.Year(), int(date.Month())} +} + +func (m Month) String() string { + return fmt.Sprintf("%d-%d", m.Year, m.Month) +} + +func (m Month) FirstOfMonth() time.Time { + return time.Date(m.Year, time.Month(m.Month), 1, 0, 0, 0, 0, time.Now().Location()) +} + +func (m Month) InFuture(date time.Time) bool { + if m.Year < date.Year() { + return true + } + + if m.Year > date.Year() { + return false + } + + return time.Month(m.Month) < date.Month() +} + +func (m Month) InPast(date time.Time) bool { + if m.Year > date.Year() { + return true + } + + if m.Year < date.Year() { + return false + } + + return time.Month(m.Month) > date.Month() +} + +func (m Month) InPresent(date time.Time) bool { + if date.Year() != m.Year { + return false + } + + return date.Month() == time.Month(m.Month) +} diff --git a/server/month_test.go b/server/month_test.go new file mode 100644 index 0000000..1c56845 --- /dev/null +++ b/server/month_test.go @@ -0,0 +1,46 @@ +package server_test + +import ( + "testing" + "time" + + "git.javil.eu/jacob1123/budgeteer/server" +) + +type TestCaseCompare struct { + Value server.Month + Date time.Time + InPast bool + InPresent bool + InFuture bool +} + +func TestComparisons(t *testing.T) { + t.Parallel() + + loc := time.Now().Location() + tests := []TestCaseCompare{ + {server.Month{2022, 2}, time.Date(2022, 3, 1, 0, 0, 0, 0, loc), false, false, true}, + {server.Month{2022, 3}, time.Date(2022, 3, 1, 0, 0, 0, 0, loc), false, true, false}, + {server.Month{2022, 4}, time.Date(2022, 3, 1, 0, 0, 0, 0, loc), true, false, false}, + {server.Month{2022, 2}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true}, + {server.Month{2022, 3}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, true, false}, + {server.Month{2022, 4}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false}, + {server.Month{2021, 2}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true}, + {server.Month{2021, 3}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true}, + {server.Month{2021, 4}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true}, + {server.Month{2023, 2}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false}, + {server.Month{2023, 3}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false}, + {server.Month{2023, 4}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false}, + {server.Month{2021, 11}, time.Date(2021, 12, 1, 0, 0, 0, 0, loc), false, false, true}, + } + for i := range tests { //nolint:paralleltest + test := tests[i] + t.Run(test.Date.Format("2006-01-02")+" is in of "+test.Value.String(), func(t *testing.T) { + t.Parallel() + server.AssertEqualBool(t, test.InPast, test.Value.InPast(test.Date), "in past") + server.AssertEqualBool(t, test.InPresent, test.Value.InPresent(test.Date), "in present") + server.AssertEqualBool(t, test.InFuture, test.Value.InFuture(test.Date), "in future") + }) + } +} diff --git a/server/util.go b/server/util.go index 531a88e..34955b8 100644 --- a/server/util.go +++ b/server/util.go @@ -8,74 +8,6 @@ import ( "github.com/gin-gonic/gin" ) -type Month struct { - Year int - Month int -} - -func NewFromTime(date time.Time) Month { - return Month{date.Year(), int(date.Month())} -} - -func (m Month) String() string { - return fmt.Sprintf("%d-%d", m.Year, m.Month) -} - -func (m Month) FirstOfMonth() time.Time { - return time.Date(m.Year, time.Month(m.Month), 1, 0, 0, 0, 0, time.Now().Location()) -} - -func (m Month) nextMonth() Month { - if m.Month == 12 { - return Month{m.Year + 1, 1} - } - return Month{m.Year, m.Month} -} - -func (m Month) InFuture(date time.Time) bool { - if m.Year < date.Year() { - return true - } - - if m.Year > date.Year() { - return false - } - - return time.Month(m.Month) < date.Month() -} - -func (m Month) isBeforeOrContains(date time.Time) bool { - if m.Year < date.Year() { - return true - } - - if m.Year > date.Year() { - return false - } - - return time.Month(m.Month) <= date.Month() -} - -func (m Month) InPast(date time.Time) bool { - if m.Year > date.Year() { - return false - } - - if m.Year < date.Year() { - return true - } - - return time.Month(m.Month) > date.Month() -} - -func (m Month) InPresent(date time.Time) bool { - if date.Year() != m.Year { - return false - } - - return date.Month() == time.Month(m.Month) -} - func getDate(c *gin.Context) (Month, error) { var year, month int yearString := c.Param("year") From ed321d3895c61ea48270776b58b491198acc4e52 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Thu, 7 Apr 2022 07:52:06 +0000 Subject: [PATCH 06/28] Fix income category not being correct for newly created budget --- postgres/budgetservice.go | 1 + 1 file changed, 1 insertion(+) diff --git a/postgres/budgetservice.go b/postgres/budgetservice.go index 82372dd..58918df 100644 --- a/postgres/budgetservice.go +++ b/postgres/budgetservice.go @@ -59,5 +59,6 @@ func (s *Database) NewBudget(context context.Context, name string, userID uuid.U return nil, fmt.Errorf("commit: %w", err) } + budget.IncomeCategoryID = cat.ID return &budget, nil } From a2280e50ecfbb64f9fd587b5eb06487b435c78e2 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Thu, 7 Apr 2022 07:53:25 +0000 Subject: [PATCH 07/28] Fix test without available property --- server/budgeting.go | 29 +++++++++++++++-------------- server/main_test.go | 4 +++- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/server/budgeting.go b/server/budgeting.go index 5960a87..9e0a7d1 100644 --- a/server/budgeting.go +++ b/server/budgeting.go @@ -13,17 +13,17 @@ import ( type CategoryWithBalance struct { *postgres.GetCategoriesRow - Available numeric.Numeric - Activity numeric.Numeric - Assigned numeric.Numeric + AvailableLastMonth numeric.Numeric + Activity numeric.Numeric + Assigned numeric.Numeric } func NewCategoryWithBalance(category *postgres.GetCategoriesRow) CategoryWithBalance { return CategoryWithBalance{ - GetCategoriesRow: category, - Available: numeric.Zero(), - Activity: numeric.Zero(), - Assigned: numeric.Zero(), + GetCategoriesRow: category, + AvailableLastMonth: numeric.Zero(), + Activity: numeric.Zero(), + Assigned: numeric.Zero(), } } @@ -157,16 +157,17 @@ func (h *Handler) calculateBalances(budget postgres.Budget, month Month, } moneyUsed.SubI(bal.Assignments) - categoryWithBalance.Available.AddI(bal.Assignments) - categoryWithBalance.Available.AddI(bal.Transactions) - if !categoryWithBalance.Available.IsPositive() && month.InPast(bal.Date) { - moneyUsed.AddI(categoryWithBalance.Available) - categoryWithBalance.Available = numeric.Zero() - } - if month.InPresent(bal.Date) { categoryWithBalance.Activity = bal.Transactions categoryWithBalance.Assigned = bal.Assignments + continue + } + + categoryWithBalance.AvailableLastMonth.AddI(bal.Assignments) + categoryWithBalance.AvailableLastMonth.AddI(bal.Transactions) + if !categoryWithBalance.AvailableLastMonth.IsPositive() { + moneyUsed.AddI(categoryWithBalance.AvailableLastMonth) + categoryWithBalance.AvailableLastMonth = numeric.Zero() } } diff --git a/server/main_test.go b/server/main_test.go index 00b31fc..772bbe0 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -231,9 +231,11 @@ func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCa name := category.Group + " : " + category.Name if name == categoryName { - assertEqual(t, categoryTestData.Available, category.Available.GetFloat64(), "available for "+categoryName) assertEqual(t, categoryTestData.Activity, category.Activity.GetFloat64(), "activity for "+categoryName) assertEqual(t, categoryTestData.Assigned, category.Assigned.GetFloat64(), "assigned for "+categoryName) + available := category.AvailableLastMonth + available.AddI(category.Activity).AddI(category.Assigned) + assertEqual(t, categoryTestData.Available, available.GetFloat64(), "available for "+categoryName) found = true } } From 11df0fbff1f277063ed4c563199d543a60aefa80 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Thu, 7 Apr 2022 07:53:48 +0000 Subject: [PATCH 08/28] Remove unused cumultative columns --- postgres/cumultative-balances.sql.go | 18 +++++++----------- postgres/queries/cumultative-balances.sql | 6 +++--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/postgres/cumultative-balances.sql.go b/postgres/cumultative-balances.sql.go index 3b4b636..ea7e6ef 100644 --- a/postgres/cumultative-balances.sql.go +++ b/postgres/cumultative-balances.sql.go @@ -13,21 +13,19 @@ import ( const getCumultativeBalances = `-- name: GetCumultativeBalances :many SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id), - COALESCE(ass.amount, 0)::decimal(12,2) as assignments, SUM(ass.amount) OVER (PARTITION BY ass.category_id ORDER BY ass.date)::decimal(12,2) as assignments_cum, - COALESCE(tra.amount, 0)::decimal(12,2) as transactions, SUM(tra.amount) OVER (PARTITION BY tra.category_id ORDER BY tra.date)::decimal(12,2) as transactions_cum + COALESCE(ass.amount, 0)::decimal(12,2) as assignments, + COALESCE(tra.amount, 0)::decimal(12,2) as transactions FROM assignments_by_month as ass FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id WHERE (ass.budget_id IS NULL OR ass.budget_id = $1) AND (tra.budget_id IS NULL OR tra.budget_id = $1) -ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id) +ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.amount, tra.amount) ` type GetCumultativeBalancesRow struct { - Date time.Time - CategoryID uuid.UUID - Assignments numeric.Numeric - AssignmentsCum numeric.Numeric - Transactions numeric.Numeric - TransactionsCum numeric.Numeric + Date time.Time + CategoryID uuid.UUID + Assignments numeric.Numeric + Transactions numeric.Numeric } func (q *Queries) GetCumultativeBalances(ctx context.Context, budgetID uuid.UUID) ([]GetCumultativeBalancesRow, error) { @@ -43,9 +41,7 @@ func (q *Queries) GetCumultativeBalances(ctx context.Context, budgetID uuid.UUID &i.Date, &i.CategoryID, &i.Assignments, - &i.AssignmentsCum, &i.Transactions, - &i.TransactionsCum, ); err != nil { return nil, err } diff --git a/postgres/queries/cumultative-balances.sql b/postgres/queries/cumultative-balances.sql index 5ba6f72..cc22480 100644 --- a/postgres/queries/cumultative-balances.sql +++ b/postgres/queries/cumultative-balances.sql @@ -1,8 +1,8 @@ -- name: GetCumultativeBalances :many SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id), - COALESCE(ass.amount, 0)::decimal(12,2) as assignments, SUM(ass.amount) OVER (PARTITION BY ass.category_id ORDER BY ass.date)::decimal(12,2) as assignments_cum, - COALESCE(tra.amount, 0)::decimal(12,2) as transactions, SUM(tra.amount) OVER (PARTITION BY tra.category_id ORDER BY tra.date)::decimal(12,2) as transactions_cum + COALESCE(ass.amount, 0)::decimal(12,2) as assignments, + COALESCE(tra.amount, 0)::decimal(12,2) as transactions FROM assignments_by_month as ass FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id WHERE (ass.budget_id IS NULL OR ass.budget_id = @budget_id) AND (tra.budget_id IS NULL OR tra.budget_id = @budget_id) -ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id); \ No newline at end of file +ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.amount, tra.amount); \ No newline at end of file From 4f681d6d89b9d007e846363745fd1ff29a682668 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Thu, 7 Apr 2022 07:54:23 +0000 Subject: [PATCH 09/28] Update testdata --- testdata | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata b/testdata index 6ca3adc..32707f2 160000 --- a/testdata +++ b/testdata @@ -1 +1 @@ -Subproject commit 6ca3adcee2713e8205133bec6c24b45aa8d730d9 +Subproject commit 32707f2528f5b31ca6d55166cc9a1aea908e37ae From bb664494f6763b5d5bb1a91a68b228661152f908 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Thu, 7 Apr 2022 08:06:58 +0000 Subject: [PATCH 10/28] Use available from API instead of category --- web/src/stores/budget-account.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/web/src/stores/budget-account.ts b/web/src/stores/budget-account.ts index 97d6dd3..6fe0d0e 100644 --- a/web/src/stores/budget-account.ts +++ b/web/src/stores/budget-account.ts @@ -9,6 +9,7 @@ interface State { CurrentAccountID: string | null; Categories: Map; Months: Map>>; + Available: Map>; Assignments: []; } @@ -43,6 +44,7 @@ export const useAccountStore = defineStore("budget/account", { Accounts: new Map(), CurrentAccountID: null, Months: new Map>>(), + Available: new Map>(), Categories: new Map(), Assignments: [], }), @@ -96,15 +98,8 @@ export const useAccountStore = defineStore("budget/account", { }, GetIncomeAvailable(state) { return (year: number, month: number) => { - const IncomeCategoryID = this.GetIncomeCategoryID; - if (IncomeCategoryID == null) return 0; - - const categories = this.AllCategoriesForMonth(year, month); - const category = categories.filter( - (x) => x.ID == IncomeCategoryID - )[0]; - if (category == null) return 0; - return category.AvailableLastMonth; + const yearMapAv = this.Available.get(year); + return yearMapAv?.get(month); }; }, CategoryGroupsForMonth(state) { @@ -210,7 +205,7 @@ export const useAccountStore = defineStore("budget/account", { response.Categories.length <= 0 ) return; - this.addCategoriesForMonth(year, month, response.Categories); + this.addCategoriesForMonth(year, month, response.Categories, response.AvailableBalance); }, async EditAccount( accountid: string, @@ -236,7 +231,8 @@ export const useAccountStore = defineStore("budget/account", { addCategoriesForMonth( year: number, month: number, - categories: Category[] + categories: Category[], + available: number ): void { this.$patch((state) => { const yearMap = @@ -250,6 +246,12 @@ export const useAccountStore = defineStore("budget/account", { yearMap.set(month, monthMap); state.Months.set(year, yearMap); + + const yearMapAv = + state.Available.get(year) || + new Map(); + yearMapAv.set(month, available); + state.Available.set(year, yearMapAv); }); }, logout() { From 76ee88a1c6ab2bbaae1e1f0e1b5697685da4e14a Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Thu, 7 Apr 2022 08:16:45 +0000 Subject: [PATCH 11/28] Update testdata --- testdata | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata b/testdata index 32707f2..28bef31 160000 --- a/testdata +++ b/testdata @@ -1 +1 @@ -Subproject commit 32707f2528f5b31ca6d55166cc9a1aea908e37ae +Subproject commit 28bef3170080056a05ba1dba70b717155f7c9310 From 249aa534c8b34e001af6424abe90a1821d057e40 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Fri, 8 Apr 2022 18:08:35 +0000 Subject: [PATCH 12/28] Update testdata --- testdata | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata b/testdata index 28bef31..8de369b 160000 --- a/testdata +++ b/testdata @@ -1 +1 @@ -Subproject commit 28bef3170080056a05ba1dba70b717155f7c9310 +Subproject commit 8de369b17a81f2e6ed079374ab35f868f259f9c1 From 3ca7bfeade021e3eb15a88216ac18d2a40a87772 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Fri, 8 Apr 2022 18:23:29 +0000 Subject: [PATCH 13/28] Fix number-input not returning numbers --- server/category.go | 2 +- web/src/components/Input.vue | 22 ++++++++++++++++------ web/src/stores/budget-account.ts | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/server/category.go b/server/category.go index 1881f2a..7507cb7 100644 --- a/server/category.go +++ b/server/category.go @@ -11,7 +11,7 @@ import ( ) type SetCategoryAssignmentRequest struct { - Assigned string + Assigned float64 } func (h *Handler) setCategoryAssignment(c *gin.Context) { diff --git a/web/src/components/Input.vue b/web/src/components/Input.vue index 68e1dae..a140cea 100644 --- a/web/src/components/Input.vue +++ b/web/src/components/Input.vue @@ -1,17 +1,27 @@ diff --git a/web/src/stores/budget-account.ts b/web/src/stores/budget-account.ts index 6fe0d0e..1a69526 100644 --- a/web/src/stores/budget-account.ts +++ b/web/src/stores/budget-account.ts @@ -61,7 +61,7 @@ export const useAccountStore = defineStore("budget/account", { return (category: Category): number => { return ( category.AvailableLastMonth + - Number(category.Assigned) + + category.Assigned + category.Activity ); }; From 23bd12147cb3ffc7799c4bcd2c9518e892f9bc0b Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Fri, 8 Apr 2022 18:23:39 +0000 Subject: [PATCH 14/28] Fix redirecting after deleting budget --- web/src/pages/Settings.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/pages/Settings.vue b/web/src/pages/Settings.vue index 55a9a94..3b99c60 100644 --- a/web/src/pages/Settings.vue +++ b/web/src/pages/Settings.vue @@ -9,6 +9,8 @@ import Button from "../components/SimpleButton.vue"; import { saveAs } from 'file-saver'; import Input from "../components/Input.vue"; +const router = useRouter(); + const transactionsFile = ref(undefined); const assignmentsFile = ref(undefined); @@ -39,7 +41,7 @@ function deleteBudget() { const budgetStore = useSessionStore(); budgetStore.Budgets.delete(CurrentBudgetID.value); - useRouter().push("/") + router.push("/dashboard") }; function clearBudget() { POST("/budget/" + CurrentBudgetID.value + "/settings/clear", null) From c4fc80e47d5a18afefe1cdaf3d5e232e740912fc Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Fri, 8 Apr 2022 18:28:52 +0000 Subject: [PATCH 15/28] Add type-information to GetBudgeted --- web/src/components/TransactionRow.vue | 1 - web/src/pages/Budgeting.vue | 6 +++--- web/src/stores/budget-account.ts | 16 +++++++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/web/src/components/TransactionRow.vue b/web/src/components/TransactionRow.vue index 5f6f561..82320e4 100644 --- a/web/src/components/TransactionRow.vue +++ b/web/src/components/TransactionRow.vue @@ -6,7 +6,6 @@ import Currency from "./Currency.vue"; import TransactionEditRow from "./TransactionEditRow.vue"; import { formatDate } from "../date"; import { useAccountStore } from "../stores/budget-account"; -import Input from "./Input.vue"; import Checkbox from "./Checkbox.vue"; const props = defineProps<{ diff --git a/web/src/pages/Budgeting.vue b/web/src/pages/Budgeting.vue index 8b4dd8a..d0328f6 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -83,17 +83,17 @@ function assignedChanged(e : Event, category : Category){
Budgeted this month:
Deassigned this month:
Spent this month:
diff --git a/web/src/stores/budget-account.ts b/web/src/stores/budget-account.ts index 1a69526..7b08cb9 100644 --- a/web/src/stores/budget-account.ts +++ b/web/src/stores/budget-account.ts @@ -39,6 +39,12 @@ export interface Category { Activity: number; } +interface BudgetedAmounts { + Assigned: number, + Deassigned: number, + Activity: number, +} + export const useAccountStore = defineStore("budget/account", { state: (): State => ({ Accounts: new Map(), @@ -71,9 +77,9 @@ export const useAccountStore = defineStore("budget/account", { return budget.CurrentBudget?.IncomeCategoryID; }, GetBudgeted(state) { - return (year: number, month: number) => { + return (year: number, month: number) : BudgetedAmounts => { const IncomeCategoryID = this.GetIncomeCategoryID; - if (IncomeCategoryID == null) return 0; + if (IncomeCategoryID == null) return {Activity: 0, Assigned: 0, Deassigned: 0}; const categories = this.AllCategoriesForMonth(year, month); @@ -90,9 +96,9 @@ export const useAccountStore = defineStore("budget/account", { deassigned += category.Assigned; } return { - assigned, - deassigned, - activity + Assigned: assigned, + Deassigned: deassigned, + Activity: activity }; }; }, From 08f48a63adf862285ca8d4f45673affebc350f56 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Fri, 8 Apr 2022 18:56:36 +0000 Subject: [PATCH 16/28] Split activity into income and spending --- web/src/pages/Budgeting.vue | 25 +++++++++++++++---------- web/src/stores/budget-account.ts | 15 ++++++++++----- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/web/src/pages/Budgeting.vue b/web/src/pages/Budgeting.vue index d0328f6..37e6810 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -72,28 +72,33 @@ function assignedChanged(e : Event, category : Category){ POST("/budget/"+CurrentBudgetID.value+"/category/" + category.ID + "/" + selected.value.Year + "/" + (selected.value.Month+1), JSON.stringify({Assigned: category.Assigned})); } + +const budgeted = computed(() => accountStore.GetBudgeted(selected.value.Year, selected.value.Month))