From 0ee3f269b581d8c326d68bd9c4ccddf998bf0050 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 7 Dec 2021 15:42:20 +0000 Subject: [PATCH] Split routes into own files --- http/account.go | 45 ++++++ http/accounts.go | 38 ----- http/admin.go | 27 ++++ http/budget.go | 16 ++ http/budgeting.go | 26 --- http/http.go | 120 +------------- http/transaction.go | 54 +++++++ http/ynab-import.go | 345 +++++++--------------------------------- postgres/ynab-import.go | 298 ++++++++++++++++++++++++++++++++++ 9 files changed, 497 insertions(+), 472 deletions(-) create mode 100644 http/account.go create mode 100644 http/transaction.go create mode 100644 postgres/ynab-import.go diff --git a/http/account.go b/http/account.go new file mode 100644 index 0000000..f321947 --- /dev/null +++ b/http/account.go @@ -0,0 +1,45 @@ +package http + +import ( + "net/http" + + "git.javil.eu/jacob1123/budgeteer/postgres" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type AccountData struct { + AlwaysNeededData + Account *postgres.Account + Transactions []postgres.GetTransactionsForAccountRow +} + +func (h *Handler) account(c *gin.Context) { + + accountID := c.Param("accountid") + accountUUID, err := uuid.Parse(accountID) + if err != nil { + c.Redirect(http.StatusTemporaryRedirect, "/login") + return + } + + account, err := h.Service.DB.GetAccount(c.Request.Context(), accountUUID) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + + transactions, err := h.Service.DB.GetTransactionsForAccount(c.Request.Context(), accountUUID) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + + d := AccountData{ + c.MustGet("data").(AlwaysNeededData), + &account, + transactions, + } + + c.HTML(http.StatusOK, "account.html", d) +} diff --git a/http/accounts.go b/http/accounts.go index 09fe886..20d833c 100644 --- a/http/accounts.go +++ b/http/accounts.go @@ -3,9 +3,7 @@ package http import ( "net/http" - "git.javil.eu/jacob1123/budgeteer/postgres" "github.com/gin-gonic/gin" - "github.com/google/uuid" ) type AccountsData struct { @@ -19,39 +17,3 @@ func (h *Handler) accounts(c *gin.Context) { c.HTML(http.StatusOK, "accounts.html", d) } - -type AccountData struct { - AlwaysNeededData - Account *postgres.Account - Transactions []postgres.GetTransactionsForAccountRow -} - -func (h *Handler) account(c *gin.Context) { - - accountID := c.Param("accountid") - accountUUID, err := uuid.Parse(accountID) - if err != nil { - c.Redirect(http.StatusTemporaryRedirect, "/login") - return - } - - account, err := h.Service.DB.GetAccount(c.Request.Context(), accountUUID) - if err != nil { - c.AbortWithError(http.StatusNotFound, err) - return - } - - transactions, err := h.Service.DB.GetTransactionsForAccount(c.Request.Context(), accountUUID) - if err != nil { - c.AbortWithError(http.StatusNotFound, err) - return - } - - d := AccountData{ - c.MustGet("data").(AlwaysNeededData), - &account, - transactions, - } - - c.HTML(http.StatusOK, "account.html", d) -} diff --git a/http/admin.go b/http/admin.go index fe00bb3..d7b4467 100644 --- a/http/admin.go +++ b/http/admin.go @@ -1,9 +1,11 @@ package http import ( + "fmt" "net/http" "github.com/gin-gonic/gin" + "github.com/google/uuid" "github.com/pressly/goose/v3" ) @@ -29,3 +31,28 @@ func (h *Handler) clearDatabase(c *gin.Context) { c.HTML(http.StatusOK, "admin.html", d) } + +func (h *Handler) clearBudget(c *gin.Context) { + budgetID := c.Param("budgetid") + budgetUUID, err := uuid.Parse(budgetID) + if err != nil { + c.Redirect(http.StatusTemporaryRedirect, "/login") + return + } + + rows, err := h.Service.DB.DeleteAllAssignments(c.Request.Context(), budgetUUID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + fmt.Printf("Deleted %d assignments\n", rows) + + rows, err = h.Service.DB.DeleteAllTransactions(c.Request.Context(), budgetUUID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + fmt.Printf("Deleted %d transactions\n", rows) +} diff --git a/http/budget.go b/http/budget.go index 71560f3..8d69990 100644 --- a/http/budget.go +++ b/http/budget.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "git.javil.eu/jacob1123/budgeteer" "git.javil.eu/jacob1123/budgeteer/postgres" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -35,3 +36,18 @@ func (h *Handler) budget(c *gin.Context) { c.HTML(http.StatusOK, "budget.html", d) } + +func (h *Handler) newBudget(c *gin.Context) { + budgetName, succ := c.GetPostForm("name") + if !succ { + c.AbortWithStatus(http.StatusNotAcceptable) + return + } + + userID := c.MustGet("token").(budgeteer.Token).GetID() + _, err := h.Service.NewBudget(budgetName, userID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } +} diff --git a/http/budgeting.go b/http/budgeting.go index 9d87085..f12f6d6 100644 --- a/http/budgeting.go +++ b/http/budgeting.go @@ -2,7 +2,6 @@ package http import ( "context" - "fmt" "net/http" "strconv" "time" @@ -76,28 +75,3 @@ func (h *Handler) budgeting(c *gin.Context) { c.HTML(http.StatusOK, "budgeting.html", d) } - -func (h *Handler) clearBudget(c *gin.Context) { - budgetID := c.Param("budgetid") - budgetUUID, err := uuid.Parse(budgetID) - if err != nil { - c.Redirect(http.StatusTemporaryRedirect, "/login") - return - } - - rows, err := h.Service.DB.DeleteAllAssignments(c.Request.Context(), budgetUUID) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - fmt.Printf("Deleted %d assignments\n", rows) - - rows, err = h.Service.DB.DeleteAllTransactions(c.Request.Context(), budgetUUID) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - fmt.Printf("Deleted %d transactions\n", rows) -} diff --git a/http/http.go b/http/http.go index 20b88db..608c807 100644 --- a/http/http.go +++ b/http/http.go @@ -1,10 +1,8 @@ package http import ( - "fmt" "io/fs" "net/http" - "time" "git.javil.eu/jacob1123/budgeteer" "git.javil.eu/jacob1123/budgeteer/bcrypt" @@ -12,7 +10,6 @@ import ( "git.javil.eu/jacob1123/budgeteer/web" "github.com/gin-gonic/gin" - "github.com/google/uuid" ) // Handler handles incoming requests @@ -58,11 +55,11 @@ func (h *Handler) Serve() { withBudget.Use(h.verifyLoginWithRedirect) withBudget.Use(h.getImportantData) withBudget.GET("/budget/:budgetid", h.budgeting) - withBudget.GET("/budget/:budgetid/clear", h.clearBudget) withBudget.GET("/budget/:budgetid/:year/:month", h.budgeting) withBudget.GET("/budget/:budgetid/all-accounts", h.budget) withBudget.GET("/budget/:budgetid/accounts", h.accounts) withBudget.GET("/budget/:budgetid/account/:accountid", h.account) + withBudget.GET("/budget/:budgetid/clear", h.clearBudget) api := router.Group("/api/v1") @@ -86,118 +83,3 @@ func (h *Handler) Serve() { router.Run(":1323") } - -func (h *Handler) importYNAB(c *gin.Context) { - budgetID, succ := c.GetPostForm("budget_id") - if !succ { - c.AbortWithError(http.StatusBadRequest, fmt.Errorf("no budget_id specified")) - return - } - - budgetUUID, err := uuid.Parse(budgetID) - if !succ { - c.AbortWithError(http.StatusBadRequest, err) - return - } - - ynab, err := NewYNABImport(h.Service.DB, budgetUUID) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - transactionsFile, err := c.FormFile("transactions") - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - transactions, err := transactionsFile.Open() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - err = ynab.ImportTransactions(transactions) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - assignmentsFile, err := c.FormFile("assignments") - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - assignments, err := assignmentsFile.Open() - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - - err = ynab.ImportAssignments(assignments) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } -} - -func (h *Handler) newTransaction(c *gin.Context) { - transactionMemo, succ := c.GetPostForm("memo") - if !succ { - c.AbortWithStatus(http.StatusNotAcceptable) - return - } - - transactionAccount, succ := c.GetPostForm("account_id") - if !succ { - c.AbortWithStatus(http.StatusNotAcceptable) - return - } - - transactionAccountID, err := uuid.Parse(transactionAccount) - if !succ { - c.AbortWithStatus(http.StatusNotAcceptable) - return - } - - transactionDate, succ := c.GetPostForm("date") - if !succ { - c.AbortWithStatus(http.StatusNotAcceptable) - return - } - - transactionDateValue, err := time.Parse("2006-01-02", transactionDate) - if err != nil { - c.AbortWithStatus(http.StatusNotAcceptable) - return - } - - new := postgres.CreateTransactionParams{ - Memo: transactionMemo, - Date: transactionDateValue, - Amount: postgres.Numeric{}, - AccountID: transactionAccountID, - } - _, err = h.Service.DB.CreateTransaction(c.Request.Context(), new) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } -} - -func (h *Handler) newBudget(c *gin.Context) { - budgetName, succ := c.GetPostForm("name") - if !succ { - c.AbortWithStatus(http.StatusNotAcceptable) - return - } - - userID := c.MustGet("token").(budgeteer.Token).GetID() - _, err := h.Service.NewBudget(budgetName, userID) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } -} diff --git a/http/transaction.go b/http/transaction.go new file mode 100644 index 0000000..7d670c2 --- /dev/null +++ b/http/transaction.go @@ -0,0 +1,54 @@ +package http + +import ( + "net/http" + "time" + + "git.javil.eu/jacob1123/budgeteer/postgres" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func (h *Handler) newTransaction(c *gin.Context) { + transactionMemo, succ := c.GetPostForm("memo") + if !succ { + c.AbortWithStatus(http.StatusNotAcceptable) + return + } + + transactionAccount, succ := c.GetPostForm("account_id") + if !succ { + c.AbortWithStatus(http.StatusNotAcceptable) + return + } + + transactionAccountID, err := uuid.Parse(transactionAccount) + if !succ { + c.AbortWithStatus(http.StatusNotAcceptable) + return + } + + transactionDate, succ := c.GetPostForm("date") + if !succ { + c.AbortWithStatus(http.StatusNotAcceptable) + return + } + + transactionDateValue, err := time.Parse("2006-01-02", transactionDate) + if err != nil { + c.AbortWithStatus(http.StatusNotAcceptable) + return + } + + new := postgres.CreateTransactionParams{ + Memo: transactionMemo, + Date: transactionDateValue, + Amount: postgres.Numeric{}, + AccountID: transactionAccountID, + } + _, err = h.Service.DB.CreateTransaction(c.Request.Context(), new) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } +} diff --git a/http/ynab-import.go b/http/ynab-import.go index 6eb48a0..6060bb2 100644 --- a/http/ynab-import.go +++ b/http/ynab-import.go @@ -1,299 +1,66 @@ package http import ( - "context" - "encoding/csv" "fmt" - "io" - "strings" - "time" - "unicode/utf8" + "net/http" "git.javil.eu/jacob1123/budgeteer/postgres" + "github.com/gin-gonic/gin" "github.com/google/uuid" ) -type YNABImport struct { - Context context.Context - accounts []postgres.Account - payees []postgres.Payee - categories []postgres.GetCategoriesRow - categoryGroups []postgres.CategoryGroup - queries *postgres.Queries - budgetID uuid.UUID -} - -func NewYNABImport(q *postgres.Queries, budgetID uuid.UUID) (*YNABImport, error) { - accounts, err := q.GetAccounts(context.Background(), budgetID) - if err != nil { - return nil, err - } - - payees, err := q.GetPayees(context.Background(), budgetID) - if err != nil { - return nil, err - } - - categories, err := q.GetCategories(context.Background(), budgetID) - if err != nil { - return nil, err - } - - categoryGroups, err := q.GetCategoryGroups(context.Background(), budgetID) - if err != nil { - return nil, err - } - - return &YNABImport{ - Context: context.Background(), - accounts: accounts, - payees: payees, - categories: categories, - categoryGroups: categoryGroups, - queries: q, - budgetID: budgetID, - }, nil - -} - -func (ynab *YNABImport) ImportAssignments(r io.Reader) error { - csv := csv.NewReader(r) - csv.Comma = '\t' - csv.LazyQuotes = true - - csvData, err := csv.ReadAll() - if err != nil { - return fmt.Errorf("could not read from tsv: %w", err) - } - - count := 0 - for _, record := range csvData[1:] { - //"Month" "Category Group/Category" "Category Group" "Category" "Budgeted" "Activity" "Available" - //"Apr 2019" "Income: Next Month" "Income" "Next Month" 0,00€ 0,00€ 0,00€ - dateString := record[0] - date, err := time.Parse("Jan 2006", dateString) - if err != nil { - return fmt.Errorf("could not parse date %s: %w", dateString, err) - } - - categoryGroup, categoryName := record[2], record[3] //also in 1 joined by : - category, err := ynab.GetCategory(categoryGroup, categoryName) - if err != nil { - return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err) - } - - amountString := record[4] - amount, err := GetAmount(amountString, "0,00€") - if err != nil { - return fmt.Errorf("could not parse amount %s: %w", amountString, err) - } - - if amount.Int.Int64() == 0 { - continue - } - - assignment := postgres.CreateAssignmentParams{ - Date: date, - CategoryID: category.UUID, - Amount: amount, - } - _, err = ynab.queries.CreateAssignment(ynab.Context, assignment) - if err != nil { - return fmt.Errorf("could not save assignment %v: %w", assignment, err) - } - - count++ - } - - fmt.Printf("Imported %d assignments\n", count) - - return nil -} - -func (ynab *YNABImport) ImportTransactions(r io.Reader) error { - csv := csv.NewReader(r) - csv.Comma = '\t' - csv.LazyQuotes = true - - csvData, err := csv.ReadAll() - if err != nil { - return fmt.Errorf("could not read from tsv: %w", err) - } - - count := 0 - for _, record := range csvData[1:] { - accountName := record[0] - account, err := ynab.GetAccount(accountName) - if err != nil { - return fmt.Errorf("could not get account %s: %w", accountName, err) - } - - //flag := record[1] - - dateString := record[2] - date, err := time.Parse("02.01.2006", dateString) - if err != nil { - return fmt.Errorf("could not parse date %s: %w", dateString, err) - } - - payeeName := record[3] - payeeID, err := ynab.GetPayee(payeeName) - if err != nil { - return fmt.Errorf("could not get payee %s: %w", payeeName, err) - } - - categoryGroup, categoryName := record[5], record[6] //also in 4 joined by : - category, err := ynab.GetCategory(categoryGroup, categoryName) - if err != nil { - return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err) - } - - memo := record[7] - - outflow := record[8] - inflow := record[9] - amount, err := GetAmount(inflow, outflow) - if err != nil { - return fmt.Errorf("could not parse amount from (%s/%s): %w", inflow, outflow, err) - } - - //cleared := record[10] - - transaction := postgres.CreateTransactionParams{ - Date: date, - Memo: memo, - AccountID: account.ID, - PayeeID: payeeID, - CategoryID: category, - Amount: amount, - } - _, err = ynab.queries.CreateTransaction(ynab.Context, transaction) - if err != nil { - return fmt.Errorf("could not save transaction %v: %w", transaction, err) - } - - count++ - } - - fmt.Printf("Imported %d transactions\n", count) - - return nil -} - -func trimLastChar(s string) string { - r, size := utf8.DecodeLastRuneInString(s) - if r == utf8.RuneError && (size == 0 || size == 1) { - size = 0 - } - return s[:len(s)-size] -} - -func GetAmount(inflow string, outflow string) (postgres.Numeric, error) { - // Remove trailing currency - inflow = strings.Replace(trimLastChar(inflow), ",", ".", 1) - outflow = strings.Replace(trimLastChar(outflow), ",", ".", 1) - - num := postgres.Numeric{} - err := num.Set(inflow) - if err != nil { - return num, fmt.Errorf("Could not parse inflow %s: %w", inflow, err) - } - - // if inflow is zero, use outflow - if num.Int.Int64() != 0 { - return num, nil - } - - err = num.Set("-" + outflow) - if err != nil { - return num, fmt.Errorf("Could not parse outflow %s: %w", inflow, err) - } - return num, nil -} - -func (ynab *YNABImport) GetAccount(name string) (*postgres.Account, error) { - for _, acc := range ynab.accounts { - if acc.Name == name { - return &acc, nil - } - } - - account, err := ynab.queries.CreateAccount(ynab.Context, postgres.CreateAccountParams{Name: name, BudgetID: ynab.budgetID}) - if err != nil { - return nil, err - } - - ynab.accounts = append(ynab.accounts, account) - return &account, nil -} - -func (ynab *YNABImport) GetPayee(name string) (uuid.NullUUID, error) { - if name == "" { - return uuid.NullUUID{}, nil - } - - for _, pay := range ynab.payees { - if pay.Name == name { - return uuid.NullUUID{UUID: pay.ID, Valid: true}, nil - } - } - - payee, err := ynab.queries.CreatePayee(ynab.Context, postgres.CreatePayeeParams{Name: name, BudgetID: ynab.budgetID}) - if err != nil { - return uuid.NullUUID{}, err - } - - ynab.payees = append(ynab.payees, payee) - return uuid.NullUUID{UUID: payee.ID, Valid: true}, nil -} - -func (ynab *YNABImport) GetCategory(group string, name string) (uuid.NullUUID, error) { - if group == "" || name == "" { - return uuid.NullUUID{}, nil - } - - for _, category := range ynab.categories { - if category.Name == name && category.Group == group { - return uuid.NullUUID{UUID: category.ID, Valid: true}, nil - } - } - - for _, categoryGroup := range ynab.categoryGroups { - if categoryGroup.Name == group { - createCategory := postgres.CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID} - category, err := ynab.queries.CreateCategory(ynab.Context, createCategory) - if err != nil { - return uuid.NullUUID{}, err - } - - getCategory := postgres.GetCategoriesRow{ - ID: category.ID, - CategoryGroupID: category.CategoryGroupID, - Name: category.Name, - Group: categoryGroup.Name, - } - ynab.categories = append(ynab.categories, getCategory) - return uuid.NullUUID{UUID: category.ID, Valid: true}, nil - } - } - - categoryGroup, err := ynab.queries.CreateCategoryGroup(ynab.Context, postgres.CreateCategoryGroupParams{Name: group, BudgetID: ynab.budgetID}) - if err != nil { - return uuid.NullUUID{}, err - } - ynab.categoryGroups = append(ynab.categoryGroups, categoryGroup) - - category, err := ynab.queries.CreateCategory(ynab.Context, postgres.CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID}) - if err != nil { - return uuid.NullUUID{}, err - } - - getCategory := postgres.GetCategoriesRow{ - ID: category.ID, - CategoryGroupID: category.CategoryGroupID, - Name: category.Name, - Group: categoryGroup.Name, - } - ynab.categories = append(ynab.categories, getCategory) - return uuid.NullUUID{UUID: category.ID, Valid: true}, nil +func (h *Handler) importYNAB(c *gin.Context) { + budgetID, succ := c.GetPostForm("budget_id") + if !succ { + c.AbortWithError(http.StatusBadRequest, fmt.Errorf("no budget_id specified")) + return + } + + budgetUUID, err := uuid.Parse(budgetID) + if !succ { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + ynab, err := postgres.NewYNABImport(h.Service.DB, budgetUUID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + transactionsFile, err := c.FormFile("transactions") + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + transactions, err := transactionsFile.Open() + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + err = ynab.ImportTransactions(transactions) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + assignmentsFile, err := c.FormFile("assignments") + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + assignments, err := assignmentsFile.Open() + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + err = ynab.ImportAssignments(assignments) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } } diff --git a/postgres/ynab-import.go b/postgres/ynab-import.go new file mode 100644 index 0000000..bcb18e4 --- /dev/null +++ b/postgres/ynab-import.go @@ -0,0 +1,298 @@ +package postgres + +import ( + "context" + "encoding/csv" + "fmt" + "io" + "strings" + "time" + "unicode/utf8" + + "github.com/google/uuid" +) + +type YNABImport struct { + Context context.Context + accounts []Account + payees []Payee + categories []GetCategoriesRow + categoryGroups []CategoryGroup + queries *Queries + budgetID uuid.UUID +} + +func NewYNABImport(q *Queries, budgetID uuid.UUID) (*YNABImport, error) { + accounts, err := q.GetAccounts(context.Background(), budgetID) + if err != nil { + return nil, err + } + + payees, err := q.GetPayees(context.Background(), budgetID) + if err != nil { + return nil, err + } + + categories, err := q.GetCategories(context.Background(), budgetID) + if err != nil { + return nil, err + } + + categoryGroups, err := q.GetCategoryGroups(context.Background(), budgetID) + if err != nil { + return nil, err + } + + return &YNABImport{ + Context: context.Background(), + accounts: accounts, + payees: payees, + categories: categories, + categoryGroups: categoryGroups, + queries: q, + budgetID: budgetID, + }, nil + +} + +func (ynab *YNABImport) ImportAssignments(r io.Reader) error { + csv := csv.NewReader(r) + csv.Comma = '\t' + csv.LazyQuotes = true + + csvData, err := csv.ReadAll() + if err != nil { + return fmt.Errorf("could not read from tsv: %w", err) + } + + count := 0 + for _, record := range csvData[1:] { + //"Month" "Category Group/Category" "Category Group" "Category" "Budgeted" "Activity" "Available" + //"Apr 2019" "Income: Next Month" "Income" "Next Month" 0,00€ 0,00€ 0,00€ + dateString := record[0] + date, err := time.Parse("Jan 2006", dateString) + if err != nil { + return fmt.Errorf("could not parse date %s: %w", dateString, err) + } + + categoryGroup, categoryName := record[2], record[3] //also in 1 joined by : + category, err := ynab.GetCategory(categoryGroup, categoryName) + if err != nil { + return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err) + } + + amountString := record[4] + amount, err := GetAmount(amountString, "0,00€") + if err != nil { + return fmt.Errorf("could not parse amount %s: %w", amountString, err) + } + + if amount.Int.Int64() == 0 { + continue + } + + assignment := CreateAssignmentParams{ + Date: date, + CategoryID: category.UUID, + Amount: amount, + } + _, err = ynab.queries.CreateAssignment(ynab.Context, assignment) + if err != nil { + return fmt.Errorf("could not save assignment %v: %w", assignment, err) + } + + count++ + } + + fmt.Printf("Imported %d assignments\n", count) + + return nil +} + +func (ynab *YNABImport) ImportTransactions(r io.Reader) error { + csv := csv.NewReader(r) + csv.Comma = '\t' + csv.LazyQuotes = true + + csvData, err := csv.ReadAll() + if err != nil { + return fmt.Errorf("could not read from tsv: %w", err) + } + + count := 0 + for _, record := range csvData[1:] { + accountName := record[0] + account, err := ynab.GetAccount(accountName) + if err != nil { + return fmt.Errorf("could not get account %s: %w", accountName, err) + } + + //flag := record[1] + + dateString := record[2] + date, err := time.Parse("02.01.2006", dateString) + if err != nil { + return fmt.Errorf("could not parse date %s: %w", dateString, err) + } + + payeeName := record[3] + payeeID, err := ynab.GetPayee(payeeName) + if err != nil { + return fmt.Errorf("could not get payee %s: %w", payeeName, err) + } + + categoryGroup, categoryName := record[5], record[6] //also in 4 joined by : + category, err := ynab.GetCategory(categoryGroup, categoryName) + if err != nil { + return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err) + } + + memo := record[7] + + outflow := record[8] + inflow := record[9] + amount, err := GetAmount(inflow, outflow) + if err != nil { + return fmt.Errorf("could not parse amount from (%s/%s): %w", inflow, outflow, err) + } + + //cleared := record[10] + + transaction := CreateTransactionParams{ + Date: date, + Memo: memo, + AccountID: account.ID, + PayeeID: payeeID, + CategoryID: category, + Amount: amount, + } + _, err = ynab.queries.CreateTransaction(ynab.Context, transaction) + if err != nil { + return fmt.Errorf("could not save transaction %v: %w", transaction, err) + } + + count++ + } + + fmt.Printf("Imported %d transactions\n", count) + + return nil +} + +func trimLastChar(s string) string { + r, size := utf8.DecodeLastRuneInString(s) + if r == utf8.RuneError && (size == 0 || size == 1) { + size = 0 + } + return s[:len(s)-size] +} + +func GetAmount(inflow string, outflow string) (Numeric, error) { + // Remove trailing currency + inflow = strings.Replace(trimLastChar(inflow), ",", ".", 1) + outflow = strings.Replace(trimLastChar(outflow), ",", ".", 1) + + num := Numeric{} + err := num.Set(inflow) + if err != nil { + return num, fmt.Errorf("Could not parse inflow %s: %w", inflow, err) + } + + // if inflow is zero, use outflow + if num.Int.Int64() != 0 { + return num, nil + } + + err = num.Set("-" + outflow) + if err != nil { + return num, fmt.Errorf("Could not parse outflow %s: %w", inflow, err) + } + return num, nil +} + +func (ynab *YNABImport) GetAccount(name string) (*Account, error) { + for _, acc := range ynab.accounts { + if acc.Name == name { + return &acc, nil + } + } + + account, err := ynab.queries.CreateAccount(ynab.Context, CreateAccountParams{Name: name, BudgetID: ynab.budgetID}) + if err != nil { + return nil, err + } + + ynab.accounts = append(ynab.accounts, account) + return &account, nil +} + +func (ynab *YNABImport) GetPayee(name string) (uuid.NullUUID, error) { + if name == "" { + return uuid.NullUUID{}, nil + } + + for _, pay := range ynab.payees { + if pay.Name == name { + return uuid.NullUUID{UUID: pay.ID, Valid: true}, nil + } + } + + payee, err := ynab.queries.CreatePayee(ynab.Context, CreatePayeeParams{Name: name, BudgetID: ynab.budgetID}) + if err != nil { + return uuid.NullUUID{}, err + } + + ynab.payees = append(ynab.payees, payee) + return uuid.NullUUID{UUID: payee.ID, Valid: true}, nil +} + +func (ynab *YNABImport) GetCategory(group string, name string) (uuid.NullUUID, error) { + if group == "" || name == "" { + return uuid.NullUUID{}, nil + } + + for _, category := range ynab.categories { + if category.Name == name && category.Group == group { + return uuid.NullUUID{UUID: category.ID, Valid: true}, nil + } + } + + for _, categoryGroup := range ynab.categoryGroups { + if categoryGroup.Name == group { + createCategory := CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID} + category, err := ynab.queries.CreateCategory(ynab.Context, createCategory) + if err != nil { + return uuid.NullUUID{}, err + } + + getCategory := GetCategoriesRow{ + ID: category.ID, + CategoryGroupID: category.CategoryGroupID, + Name: category.Name, + Group: categoryGroup.Name, + } + ynab.categories = append(ynab.categories, getCategory) + return uuid.NullUUID{UUID: category.ID, Valid: true}, nil + } + } + + categoryGroup, err := ynab.queries.CreateCategoryGroup(ynab.Context, CreateCategoryGroupParams{Name: group, BudgetID: ynab.budgetID}) + if err != nil { + return uuid.NullUUID{}, err + } + ynab.categoryGroups = append(ynab.categoryGroups, categoryGroup) + + category, err := ynab.queries.CreateCategory(ynab.Context, CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID}) + if err != nil { + return uuid.NullUUID{}, err + } + + getCategory := GetCategoriesRow{ + ID: category.ID, + CategoryGroupID: category.CategoryGroupID, + Name: category.Name, + Group: categoryGroup.Name, + } + ynab.categories = append(ynab.categories, getCategory) + return uuid.NullUUID{UUID: category.ID, Valid: true}, nil +}