Compare commits
16 Commits
master
...
4ee82dce26
Author | SHA1 | Date | |
---|---|---|---|
4ee82dce26 | |||
1da86e30c3 | |||
51902cd65d | |||
014c3818ba | |||
210ddd65a5 | |||
5741236e2c | |||
99549fb441 | |||
67c79e252e | |||
3b1174225a | |||
92f56b1046 | |||
f068dd5009 | |||
ebc34d7031 | |||
03ea4a31ad | |||
8636d04b84 | |||
c63a8bc5d3 | |||
0aae7236ae |
@ -14,7 +14,6 @@ linters:
|
|||||||
- gci # not working, shows errors on freshly formatted file
|
- gci # not working, shows errors on freshly formatted file
|
||||||
- varnamelen
|
- varnamelen
|
||||||
- lll
|
- lll
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
errcheck:
|
errcheck:
|
||||||
exclude-functions:
|
exclude-functions:
|
||||||
@ -26,18 +25,3 @@ linters-settings:
|
|||||||
varnamelen:
|
varnamelen:
|
||||||
ignore-decls:
|
ignore-decls:
|
||||||
- c *gin.Context
|
- c *gin.Context
|
||||||
wrapcheck:
|
|
||||||
ignoreSigs:
|
|
||||||
- .JSON(
|
|
||||||
- .Redirect(
|
|
||||||
- .String(
|
|
||||||
- .Errorf(
|
|
||||||
- errors.New(
|
|
||||||
- errors.Unwrap(
|
|
||||||
- .Wrap(
|
|
||||||
- .Wrapf(
|
|
||||||
- .WithMessage(
|
|
||||||
- .WithMessagef(
|
|
||||||
- .WithStack(
|
|
||||||
ignorePackageGlobs:
|
|
||||||
- git.javil.eu/jacob1123/budgeteer/postgres
|
|
26
README.md
26
README.md
@ -1,18 +1,20 @@
|
|||||||
# Budgeteer
|
# Budgeteer
|
||||||
|
|
||||||
Budgeting Web-Application written in Go and inspired by [YNAB](https://youneedabudget.com).
|
Budgeting Web-Application
|
||||||
|
|
||||||
## Getting started
|
## Data structure
|
||||||
|
|
||||||
The fastest way to get up and running quickly, is using docker-compose. Just download the [docker-compose.yml](https://git.javil.eu/jacob1123/budgeteer/src/branch/master/docker/docker-compose.yml) to some empty directory and run `docker-compose up -d`. This starts budgeteer, a postgres database and an adminer instance. The latter is optional and can be removed from the docker-compose.yml.
|
1 User
|
||||||
|
N Budgets
|
||||||
|
AccountID[]
|
||||||
|
CategoryID[]
|
||||||
|
PayeeID[]
|
||||||
|
|
||||||
## Known issues
|
N Accounts
|
||||||
|
TransactionID[]
|
||||||
|
N Categories
|
||||||
|
AssignmentID[]
|
||||||
|
N Payees
|
||||||
|
|
||||||
Currently the application is usable when importing from YNAB via their CSV export. All balances should match the balances from YNAB. There are even unit-tests that confirm that using my personal budget.
|
N Transactions
|
||||||
|
N Assignments
|
||||||
For people wishing to start fresh in Budgeteer, there currently are some blockers though:
|
|
||||||
- The ability to create new accounts and categories is missing (#59)
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
If you're willing to help, please check the issues for [help-wanted labels](https://git.javil.eu/jacob1123/budgeteer/issues?labels=4). Just using Budgeteer and reporting any issues is although very helpful.
|
|
12
bass.build
12
bass.build
@ -1,12 +0,0 @@
|
|||||||
(def go
|
|
||||||
(from (linux/alpine)
|
|
||||||
($ apk add go)))
|
|
||||||
|
|
||||||
(-> ($ go mod download)
|
|
||||||
(with-image go)
|
|
||||||
(with-mount *dir*/go.mod ./go.mod)
|
|
||||||
(with-mount *dir*/go.sum ./go.sum))
|
|
||||||
|
|
||||||
(def go-mods
|
|
||||||
(from go
|
|
||||||
($ go mod download)))
|
|
@ -10,11 +10,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type FilterTransactionsRequest struct {
|
type FilterTransactionsRequest struct {
|
||||||
CategoryID string `json:"categoryId"`
|
CategoryID string `json:"category_id"`
|
||||||
PayeeID string `json:"payeeId"`
|
PayeeID string `json:"payee_id"`
|
||||||
AccountID string `json:"accountId"`
|
AccountID string `json:"account_id"`
|
||||||
FromDate time.Time `json:"fromDate"`
|
FromDate time.Time `json:"from_date"`
|
||||||
ToDate time.Time `json:"toDate"`
|
ToDate time.Time `json:"to_date"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) filteredTransactions(c echo.Context) error {
|
func (h *Handler) filteredTransactions(c echo.Context) error {
|
||||||
@ -58,7 +58,7 @@ func parseEmptyUUID(value string) (uuid.NullUUID, bool) {
|
|||||||
return uuid.NullUUID{}, false
|
return uuid.NullUUID{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return uuid.NullUUID{UUID: val, Valid: true}, true
|
return uuid.NullUUID{val, true}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) problematicTransactions(c echo.Context) error {
|
func (h *Handler) problematicTransactions(c echo.Context) error {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -36,23 +35,17 @@ func TestRegisterUser(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("RegisterUser", func(t *testing.T) {
|
t.Run("RegisterUser", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
request, err := http.NewRequestWithContext(context.Background(),
|
request, err := http.NewRequest(
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
"/api/v1/user/register",
|
"/api/v1/user/register",
|
||||||
strings.NewReader(`{"password":"pass","email":"info@example.com","name":"Test"}`))
|
strings.NewReader(`{"password":"pass","email":"info@example.com","name":"Test"}`))
|
||||||
request.Header.Set("Content-Type", "application/json")
|
|
||||||
context := engine.NewContext(request, recorder)
|
context := engine.NewContext(request, recorder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error creating request: %s", err)
|
t.Errorf("error creating request: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.registerPost(context)
|
h.registerPost(context)
|
||||||
if err != nil {
|
|
||||||
t.Error(err.Error())
|
|
||||||
t.Error("Error registering")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Errorf("handler returned wrong status code: got %v want %v", recorder.Code, http.StatusOK)
|
t.Errorf("handler returned wrong status code: got %v want %v", recorder.Code, http.StatusOK)
|
||||||
|
@ -41,7 +41,7 @@ func (h *Handler) budgetingForMonth(c echo.Context) error {
|
|||||||
|
|
||||||
month, err := getDate(c)
|
month, err := getDate(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budget.ID.String())
|
c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budget.ID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := h.getBudgetingViewForMonth(c.Request().Context(), budget, month)
|
data, err := h.getBudgetingViewForMonth(c.Request().Context(), budget, month)
|
||||||
|
@ -35,7 +35,7 @@ func (h *Handler) verifyLogin(c echo.Context) (budgeteer.Token, error) { //nolin
|
|||||||
tokenString = tokenString[7:]
|
tokenString = tokenString[7:]
|
||||||
token, err := h.TokenVerifier.VerifyToken(tokenString)
|
token, err := h.TokenVerifier.VerifyToken(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("verify token '%s': %w", tokenString, err)
|
return nil, fmt.Errorf("verify token '%s': %s", tokenString, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return token, nil
|
return token, nil
|
||||||
@ -63,7 +63,7 @@ func (h *Handler) loginPost(c echo.Context) error {
|
|||||||
var login loginInformation
|
var login loginInformation
|
||||||
err := c.Bind(&login)
|
err := c.Bind(&login)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse payload: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.Service.GetUserByUsername(c.Request().Context(), login.User)
|
user, err := h.Service.GetUserByUsername(c.Request().Context(), login.User)
|
||||||
@ -72,12 +72,12 @@ func (h *Handler) loginPost(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = h.CredentialsVerifier.Verify(login.Password, user.Password); err != nil {
|
if err = h.CredentialsVerifier.Verify(login.Password, user.Password); err != nil {
|
||||||
return fmt.Errorf("verify password: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := h.TokenVerifier.CreateToken(&user)
|
token, err := h.TokenVerifier.CreateToken(&user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create token: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go h.UpdateLastLogin(user.ID)
|
go h.UpdateLastLogin(user.ID)
|
||||||
@ -120,7 +120,7 @@ func (h *Handler) registerPost(c echo.Context) error {
|
|||||||
|
|
||||||
hash, err := h.CredentialsVerifier.Hash(register.Password)
|
hash, err := h.CredentialsVerifier.Hash(register.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("hash password: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
createUser := postgres.CreateUserParams{
|
createUser := postgres.CreateUserParams{
|
||||||
@ -135,7 +135,7 @@ func (h *Handler) registerPost(c echo.Context) error {
|
|||||||
|
|
||||||
token, err := h.TokenVerifier.CreateToken(&user)
|
token, err := h.TokenVerifier.CreateToken(&user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create token: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go h.UpdateLastLogin(user.ID)
|
go h.UpdateLastLogin(user.ID)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
@ -27,12 +26,12 @@ func (h *Handler) importYNAB(c echo.Context) error {
|
|||||||
|
|
||||||
transactionsFile, err := c.FormFile("transactions")
|
transactionsFile, err := c.FormFile("transactions")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get transactions: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
transactions, err := transactionsFile.Open()
|
transactions, err := transactionsFile.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open transactions: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ynab.ImportTransactions(c.Request().Context(), transactions)
|
err = ynab.ImportTransactions(c.Request().Context(), transactions)
|
||||||
@ -42,12 +41,12 @@ func (h *Handler) importYNAB(c echo.Context) error {
|
|||||||
|
|
||||||
assignmentsFile, err := c.FormFile("assignments")
|
assignmentsFile, err := c.FormFile("assignments")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get assignments: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
assignments, err := assignmentsFile.Open()
|
assignments, err := assignmentsFile.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open assignments: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ynab.ImportAssignments(c.Request().Context(), assignments)
|
err = ynab.ImportAssignments(c.Request().Context(), assignments)
|
||||||
|
@ -11,10 +11,10 @@ export interface Suggestion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
text: string | null,
|
text: string,
|
||||||
id: string | null,
|
id: string | undefined,
|
||||||
model: string,
|
model: string,
|
||||||
type?: string | null,
|
type?: string | undefined,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const SearchQuery = ref(props.text || "");
|
const SearchQuery = ref(props.text || "");
|
||||||
|
@ -16,9 +16,9 @@ const TX = ref<Transaction>({
|
|||||||
Memo: "",
|
Memo: "",
|
||||||
Amount: 0,
|
Amount: 0,
|
||||||
Payee: "",
|
Payee: "",
|
||||||
PayeeID: null,
|
PayeeID: undefined,
|
||||||
Category: "",
|
Category: "",
|
||||||
CategoryID: null,
|
CategoryID: undefined,
|
||||||
CategoryGroup: "",
|
CategoryGroup: "",
|
||||||
GroupID: "",
|
GroupID: "",
|
||||||
ID: "",
|
ID: "",
|
||||||
|
@ -42,7 +42,7 @@ const filters = ref({
|
|||||||
ToDate: new Date(2999,11,32),
|
ToDate: new Date(2999,11,32),
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => (filters.value.AccountID ?? "")
|
watch(() => filters.value.AccountID
|
||||||
+ filters.value.PayeeID
|
+ filters.value.PayeeID
|
||||||
+ filters.value.CategoryID
|
+ filters.value.CategoryID
|
||||||
+ filters.value.FromDate?.toISOString()
|
+ filters.value.FromDate?.toISOString()
|
||||||
|
@ -2,7 +2,7 @@ import { defineStore } from "pinia";
|
|||||||
import { GET, POST } from "../api";
|
import { GET, POST } from "../api";
|
||||||
import { useBudgetsStore } from "./budget";
|
import { useBudgetsStore } from "./budget";
|
||||||
import { useSessionStore } from "./session";
|
import { useSessionStore } from "./session";
|
||||||
import { Transaction, useTransactionsStore } from "./transactions";
|
import { useTransactionsStore } from "./transactions";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
Accounts: Map<string, Account>;
|
Accounts: Map<string, Account>;
|
||||||
@ -200,7 +200,6 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
transactionsStore.AddTransactions(
|
transactionsStore.AddTransactions(
|
||||||
response.Transactions
|
response.Transactions
|
||||||
);
|
);
|
||||||
account.Transactions = response.Transactions.map((x : Transaction) =>x.ID);
|
|
||||||
},
|
},
|
||||||
async FetchMonthBudget(budgetid: string, year: number, month: number) {
|
async FetchMonthBudget(budgetid: string, year: number, month: number) {
|
||||||
const result = await GET(
|
const result = await GET(
|
||||||
|
@ -17,12 +17,12 @@ export interface Transaction {
|
|||||||
TransferAccount: string;
|
TransferAccount: string;
|
||||||
CategoryGroup: string;
|
CategoryGroup: string;
|
||||||
Category: string;
|
Category: string;
|
||||||
CategoryID: string | null;
|
CategoryID: string | undefined;
|
||||||
Memo: string;
|
Memo: string;
|
||||||
Status: string;
|
Status: string;
|
||||||
GroupID: string;
|
GroupID: string;
|
||||||
Payee: string;
|
Payee: string;
|
||||||
PayeeID: string | null;
|
PayeeID: string | undefined;
|
||||||
Amount: number;
|
Amount: number;
|
||||||
Reconciled: boolean;
|
Reconciled: boolean;
|
||||||
Account: string;
|
Account: string;
|
||||||
@ -47,12 +47,10 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
|||||||
}
|
}
|
||||||
return reconciledBalance;
|
return reconciledBalance;
|
||||||
},
|
},
|
||||||
TransactionsByDate(state) : Record<string, Transaction[]>|undefined{
|
TransactionsByDate(state) : Record<string, Transaction[]> {
|
||||||
const accountsStore = useAccountStore();
|
const accountsStore = useAccountStore();
|
||||||
const account = accountsStore.CurrentAccount;
|
const accountID = accountsStore.CurrentAccountID;
|
||||||
if(account === undefined)
|
const allTransactions = [...this.Transactions.values()].filter(x => x.AccountID == accountID);
|
||||||
return undefined;
|
|
||||||
const allTransactions = account!.Transactions.map(x => this.Transactions.get(x) ?? {} as Transaction);
|
|
||||||
return groupBy(allTransactions, x => formatDate(x.Date));
|
return groupBy(allTransactions, x => formatDate(x.Date));
|
||||||
},
|
},
|
||||||
TransactionsList(state) : Transaction[] {
|
TransactionsList(state) : Transaction[] {
|
||||||
@ -95,11 +93,11 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
|||||||
async GetFilteredTransactions(accountID : string | null, categoryID : string | null, payeeID : string | null, fromDate : string, toDate : string) {
|
async GetFilteredTransactions(accountID : string | null, categoryID : string | null, payeeID : string | null, fromDate : string, toDate : string) {
|
||||||
const budgetStore = useBudgetsStore();
|
const budgetStore = useBudgetsStore();
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
categoryId: categoryID,
|
category_id: categoryID,
|
||||||
payeeId: payeeID,
|
payee_id: payeeID,
|
||||||
accountId: accountID,
|
account_id: accountID,
|
||||||
fromDate: fromDate,
|
from_date: fromDate,
|
||||||
toDate: toDate,
|
to_date: toDate,
|
||||||
});
|
});
|
||||||
const result = await POST("/budget/" + budgetStore.CurrentBudgetID + "/filtered-transactions", payload);
|
const result = await POST("/budget/" + budgetStore.CurrentBudgetID + "/filtered-transactions", payload);
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
|
Reference in New Issue
Block a user