diff --git a/Taskfile.yml b/Taskfile.yml index 92e2fdc..f64d57a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,30 +5,31 @@ vars: tasks: default: + desc: Build budgeteer in production mode + deps: [frontend, go-mod, go-sqlc] cmds: - - task: build-prod + - task: backend - sqlc: - desc: sqlc code generation - sources: - - ./sqlc.yaml - - ./postgres/schema/* - - ./postgres/queries/* - generates: - - ./postgres/*.sql.go + run: + desc: Start budgeteer + deps: [backend, go-mod, go-sqlc] cmds: - - sqlc generate + - ./build/budgeteer{{exeExt}} - gomod: - desc: Go modules - sources: - - ./go.mod - - ./go.sum - method: checksum + dev: + desc: Build budgeteer in dev mode (without frontend) + deps: [go-mod, go-sqlc] cmds: - - go mod download + - task: backend - build: + ci: + desc: Run CI build + deps: [default, static] + + static: + deps: [go-lint, go-vet, go-fmt, js-tsc, js-lint, cover] + + backend: desc: Build budgeteer sources: - ./go.mod @@ -43,29 +44,37 @@ tasks: cmds: - go build -o ./build/budgeteer{{exeExt}} ./cmd/budgeteer - build-dev: - desc: Build budgeteer in dev mode - deps: [gomod, sqlc] + go-vet: cmds: - go vet - - go fmt - - golangci-lint run - - task: build - build-prod: - desc: Build budgeteer in prod mode - deps: [gomod, sqlc, frontend] + go-fmt: cmds: - - go vet - go fmt - - golangci-lint run - - task: build - ci: - desc: Run CI build + go-lint: cmds: - - task: build-prod - - task: cover + - golangci-lint run + + go-sqlc: + desc: sqlc code generation + sources: + - ./sqlc.yaml + - ./postgres/schema/* + - ./postgres/queries/* + generates: + - ./postgres/*.sql.go + cmds: + - sqlc generate + + go-mod: + desc: Go modules + sources: + - ./go.mod + - ./go.sum + method: checksum + cmds: + - go mod download cover: desc: Run test and analyze coverage @@ -75,26 +84,46 @@ tasks: frontend: desc: Build vue frontend - dir: web + deps: [js-build] sources: - web/src/**/* generates: - web/dist/**/* + + frontend-dev: + desc: Run dev-server for frontend + dir: web + cmds: + - yarn run dev + + js-build: + dir: web + deps: [js-mod] + cmds: + - yarn build + + js-mod: + run: once + sources: + - web/src/package.json + - web/src/yarn.lock + generates: + - web/node_modules/**/* + dir: web cmds: - yarn - - yarn build - - yarn run vue-tsc --noEmit - - yarn run eslint "./src/**" - - docker: - desc: Build budgeeter:latest - deps: [build-prod] - sources: - - ./build/budgeteer{{exeExt}} - - ./build/Dockerfile + + js-tsc: + dir: web + deps: [js-mod] cmds: - - docker build -t {{.IMAGE_NAME}}:latest ./build - - docker push {{.IMAGE_NAME}}:latest + - yarn run vue-tsc --noEmit + + js-lint: + dir: web + deps: [js-mod] + cmds: + - yarn run eslint "./src/**" dev-docker: desc: Build budgeeter:dev @@ -103,17 +132,11 @@ tasks: - ./web/package.json - ./web/yarn.lock cmds: - - docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile.dev - - docker push {{.IMAGE_NAME}}:dev - - run: - desc: Start budgeteer - deps: [build-dev] + - docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile.dev + - docker push {{.IMAGE_NAME}}:dev + + run-dev: + desc: Run dev environment in docker + deps: [dev-docker] cmds: - - ./build/budgeteer{{exeExt}} - - rundocker: - desc: Start docker-compose - deps: [docker] - cmds: - - docker-compose up -d + - docker-compose -f docker/docker-compose.dev.yml -p budgeteer up -d \ No newline at end of file diff --git a/cmd/budgeteer/main.go b/cmd/budgeteer/main.go index 71b6eb1..8f9d1d7 100644 --- a/cmd/budgeteer/main.go +++ b/cmd/budgeteer/main.go @@ -17,22 +17,22 @@ import ( func main() { cfg, err := config.LoadConfig() if err != nil { - log.Fatalf("Could not load config: %v", err) + log.Fatalf("load config: %v", err) } queries, err := postgres.Connect("pgx", cfg.DatabaseConnection) if err != nil { - log.Fatalf("Failed connecting to DB: %v", err) + log.Fatalf("connect to database: %v", err) } static, err := fs.Sub(web.Static, "dist") if err != nil { - panic("couldn't open static files") + panic("open static files") } tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret) if err != nil { - panic(fmt.Errorf("couldn't create token verifier: %w", err)) + panic(fmt.Errorf("create token verifier: %w", err)) } handler := &server.Handler{ diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 6b2533d..dcccf17 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -6,10 +6,10 @@ RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest FROM alpine RUN apk --no-cache add go nodejs yarn bash curl git git-perl -RUN yarn global add @vue/cli ENV PATH="/root/.yarn/bin/:${PATH}" WORKDIR /src/web ADD web/package.json web/yarn.lock /src/web/ -RUN yarn WORKDIR /src +VOLUME /go +VOLUME /.cache COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/ \ No newline at end of file diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 50d5413..d7245cf 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -6,11 +6,10 @@ services: command: task -w run ports: - 1323:1323 - user: '1000' volumes: - ~/budgeteer:/src - - ~/.go:/go - - ~/.cache:/.cache + - go-cache:/go + - yarn-cache:/.cache environment: BUDGETEER_DB: postgres://budgeteer:budgeteer@db:5432/budgeteer BUDGETEER_SESSION_SECRET: random string for JWT authorization @@ -19,13 +18,11 @@ services: frontend: image: hub.javil.eu/budgeteer:dev - command: bash -c "cd web; yarn run dev" + command: task frontend-dev ports: - 3000:3000 - user: '1000' volumes: - ~/budgeteer:/src - - ~/.cache:/.cache depends_on: - backend @@ -49,3 +46,5 @@ services: volumes: db: + go-cache: + yarn-cache: diff --git a/postgres/accounts.sql.go b/postgres/accounts.sql.go index e14b73a..e9ad575 100644 --- a/postgres/accounts.sql.go +++ b/postgres/accounts.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 // source: accounts.sql package postgres diff --git a/postgres/assignments.sql.go b/postgres/assignments.sql.go index 633b6d0..efbd1d9 100644 --- a/postgres/assignments.sql.go +++ b/postgres/assignments.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 // source: assignments.sql package postgres diff --git a/postgres/budgets.sql.go b/postgres/budgets.sql.go index 780c998..6d3183e 100644 --- a/postgres/budgets.sql.go +++ b/postgres/budgets.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 // source: budgets.sql package postgres 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 } diff --git a/postgres/categories.sql.go b/postgres/categories.sql.go index d31dd18..429ccf9 100644 --- a/postgres/categories.sql.go +++ b/postgres/categories.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 // source: categories.sql package postgres diff --git a/postgres/cumultative-balances.sql.go b/postgres/cumultative-balances.sql.go index 3b4b636..98f3aed 100644 --- a/postgres/cumultative-balances.sql.go +++ b/postgres/cumultative-balances.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 // source: cumultative-balances.sql package postgres @@ -13,21 +15,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 +43,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/db.go b/postgres/db.go index 8d02508..4593f0b 100644 --- a/postgres/db.go +++ b/postgres/db.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 package postgres diff --git a/postgres/models.go b/postgres/models.go index 8e01747..0763fd4 100644 --- a/postgres/models.go +++ b/postgres/models.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 package postgres 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/postgres/payees.sql.go b/postgres/payees.sql.go index 005562d..1e9f25b 100644 --- a/postgres/payees.sql.go +++ b/postgres/payees.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 // source: payees.sql package postgres 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 diff --git a/postgres/transactions.sql.go b/postgres/transactions.sql.go index d238098..68ec37c 100644 --- a/postgres/transactions.sql.go +++ b/postgres/transactions.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 // source: transactions.sql package postgres diff --git a/postgres/user_budgets.sql.go b/postgres/user_budgets.sql.go index 4740c0e..7ecb0dd 100644 --- a/postgres/user_budgets.sql.go +++ b/postgres/user_budgets.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 // source: user_budgets.sql package postgres diff --git a/postgres/users.sql.go b/postgres/users.sql.go index bd295b2..07b9b26 100644 --- a/postgres/users.sql.go +++ b/postgres/users.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.13.0 // source: users.sql package postgres diff --git a/server/budgeting.go b/server/budgeting.go index 814d94b..9e0a7d1 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,30 +11,19 @@ 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 - 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(), } } @@ -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(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,22 +88,14 @@ func (*Handler) getAvailableBalance(budget postgres.Budget, continue } - if !bal.Date.Before(firstOfNextMonth) { + if month.InFuture(bal.Date) { continue } 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 +133,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, month Month, categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow, ) ([]CategoryWithBalance, numeric.Numeric) { categoriesWithBalance := []CategoryWithBalance{} @@ -163,6 +141,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 { @@ -170,21 +152,22 @@ func (h *Handler) calculateBalances(firstOfNextMonth time.Time, firstOfMonth tim } // skip everything in the future - if !bal.Date.Before(firstOfNextMonth) { + if month.InFuture(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) { - moneyUsed.AddI(categoryWithBalance.Available) - categoryWithBalance.Available = numeric.Zero() - } - - if bal.Date.Year() == firstOfMonth.Year() && bal.Date.Month() == firstOfMonth.Month() { + 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/category.go b/server/category.go index ab4676c..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) { @@ -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..772bbe0 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 @@ -232,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 } } @@ -246,6 +247,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 ffb63a4..34955b8 100644 --- a/server/util.go +++ b/server/util.go @@ -8,7 +8,7 @@ import ( "github.com/gin-gonic/gin" ) -func getDate(c *gin.Context) (time.Time, error) { +func getDate(c *gin.Context) (Month, error) { var year, month int yearString := c.Param("year") monthString := c.Param("month") @@ -18,13 +18,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/testdata b/testdata index 6ca3adc..8de369b 160000 --- a/testdata +++ b/testdata @@ -1 +1 @@ -Subproject commit 6ca3adcee2713e8205133bec6c24b45aa8d730d9 +Subproject commit 8de369b17a81f2e6ed079374ab35f868f259f9c1 diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 3bfbf89..60afd68 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -7,6 +7,7 @@ module.exports = { ], rules: { // override/add rules settings here, such as: + 'vue/max-attributes-per-line': 'off' // 'vue/no-unused-vars': 'error' }, parser: "vue-eslint-parser", diff --git a/web/dist/generate-directory-for-ci b/web/dist/generate-directory-for-ci new file mode 100644 index 0000000..e69de29 diff --git a/web/package.json b/web/package.json index 0ab8faf..8e43209 100644 --- a/web/package.json +++ b/web/package.json @@ -22,9 +22,6 @@ "@types/file-saver": "^2.0.5", "@typescript-eslint/parser": "^5.13.0", "@vitejs/plugin-vue": "^2.0.0", - "@vue/cli-plugin-babel": "5.0.0-beta.7", - "@vue/cli-plugin-typescript": "~4.5.0", - "@vue/cli-service": "5.0.0-beta.7", "eslint": "^8.10.0", "eslint-plugin-vue": "^8.5.0", "prettier": "2.5.1", diff --git a/web/src/components/Input.vue b/web/src/components/Input.vue index 68e1dae..e810ba1 100644 --- a/web/src/components/Input.vue +++ b/web/src/components/Input.vue @@ -1,17 +1,27 @@ 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 5722854..09fc432 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -72,14 +72,35 @@ 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))