Split routes into own files

This commit is contained in:
2021-12-07 15:42:20 +00:00
parent 6bcf94661e
commit 0ee3f269b5
9 changed files with 497 additions and 472 deletions

45
http/account.go Normal file
View File

@ -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)
}

View File

@ -3,9 +3,7 @@ package http
import ( import (
"net/http" "net/http"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
type AccountsData struct { type AccountsData struct {
@ -19,39 +17,3 @@ func (h *Handler) accounts(c *gin.Context) {
c.HTML(http.StatusOK, "accounts.html", d) 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)
}

View File

@ -1,9 +1,11 @@
package http package http
import ( import (
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/pressly/goose/v3" "github.com/pressly/goose/v3"
) )
@ -29,3 +31,28 @@ func (h *Handler) clearDatabase(c *gin.Context) {
c.HTML(http.StatusOK, "admin.html", d) 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)
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"net/http" "net/http"
"git.javil.eu/jacob1123/budgeteer"
"git.javil.eu/jacob1123/budgeteer/postgres" "git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
@ -35,3 +36,18 @@ func (h *Handler) budget(c *gin.Context) {
c.HTML(http.StatusOK, "budget.html", d) 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
}
}

View File

@ -2,7 +2,6 @@ package http
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@ -76,28 +75,3 @@ func (h *Handler) budgeting(c *gin.Context) {
c.HTML(http.StatusOK, "budgeting.html", d) 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)
}

View File

@ -1,10 +1,8 @@
package http package http
import ( import (
"fmt"
"io/fs" "io/fs"
"net/http" "net/http"
"time"
"git.javil.eu/jacob1123/budgeteer" "git.javil.eu/jacob1123/budgeteer"
"git.javil.eu/jacob1123/budgeteer/bcrypt" "git.javil.eu/jacob1123/budgeteer/bcrypt"
@ -12,7 +10,6 @@ import (
"git.javil.eu/jacob1123/budgeteer/web" "git.javil.eu/jacob1123/budgeteer/web"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
// Handler handles incoming requests // Handler handles incoming requests
@ -58,11 +55,11 @@ func (h *Handler) Serve() {
withBudget.Use(h.verifyLoginWithRedirect) withBudget.Use(h.verifyLoginWithRedirect)
withBudget.Use(h.getImportantData) withBudget.Use(h.getImportantData)
withBudget.GET("/budget/:budgetid", h.budgeting) 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/:year/:month", h.budgeting)
withBudget.GET("/budget/:budgetid/all-accounts", h.budget) withBudget.GET("/budget/:budgetid/all-accounts", h.budget)
withBudget.GET("/budget/:budgetid/accounts", h.accounts) withBudget.GET("/budget/:budgetid/accounts", h.accounts)
withBudget.GET("/budget/:budgetid/account/:accountid", h.account) withBudget.GET("/budget/:budgetid/account/:accountid", h.account)
withBudget.GET("/budget/:budgetid/clear", h.clearBudget)
api := router.Group("/api/v1") api := router.Group("/api/v1")
@ -86,118 +83,3 @@ func (h *Handler) Serve() {
router.Run(":1323") 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
}
}

54
http/transaction.go Normal file
View File

@ -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
}
}

View File

@ -1,299 +1,66 @@
package http package http
import ( import (
"context"
"encoding/csv"
"fmt" "fmt"
"io" "net/http"
"strings"
"time"
"unicode/utf8"
"git.javil.eu/jacob1123/budgeteer/postgres" "git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
) )
type YNABImport struct { func (h *Handler) importYNAB(c *gin.Context) {
Context context.Context budgetID, succ := c.GetPostForm("budget_id")
accounts []postgres.Account if !succ {
payees []postgres.Payee c.AbortWithError(http.StatusBadRequest, fmt.Errorf("no budget_id specified"))
categories []postgres.GetCategoriesRow return
categoryGroups []postgres.CategoryGroup }
queries *postgres.Queries
budgetID uuid.UUID budgetUUID, err := uuid.Parse(budgetID)
} if !succ {
c.AbortWithError(http.StatusBadRequest, err)
func NewYNABImport(q *postgres.Queries, budgetID uuid.UUID) (*YNABImport, error) { return
accounts, err := q.GetAccounts(context.Background(), budgetID) }
if err != nil {
return nil, err ynab, err := postgres.NewYNABImport(h.Service.DB, budgetUUID)
} if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
payees, err := q.GetPayees(context.Background(), budgetID) return
if err != nil { }
return nil, err
} transactionsFile, err := c.FormFile("transactions")
if err != nil {
categories, err := q.GetCategories(context.Background(), budgetID) c.AbortWithError(http.StatusInternalServerError, err)
if err != nil { return
return nil, err }
}
transactions, err := transactionsFile.Open()
categoryGroups, err := q.GetCategoryGroups(context.Background(), budgetID) if err != nil {
if err != nil { c.AbortWithError(http.StatusInternalServerError, err)
return nil, err return
} }
return &YNABImport{ err = ynab.ImportTransactions(transactions)
Context: context.Background(), if err != nil {
accounts: accounts, c.AbortWithError(http.StatusInternalServerError, err)
payees: payees, return
categories: categories, }
categoryGroups: categoryGroups,
queries: q, assignmentsFile, err := c.FormFile("assignments")
budgetID: budgetID, if err != nil {
}, nil c.AbortWithError(http.StatusInternalServerError, err)
return
} }
func (ynab *YNABImport) ImportAssignments(r io.Reader) error { assignments, err := assignmentsFile.Open()
csv := csv.NewReader(r) if err != nil {
csv.Comma = '\t' c.AbortWithError(http.StatusInternalServerError, err)
csv.LazyQuotes = true return
}
csvData, err := csv.ReadAll()
if err != nil { err = ynab.ImportAssignments(assignments)
return fmt.Errorf("could not read from tsv: %w", err) if err != nil {
} c.AbortWithError(http.StatusInternalServerError, err)
return
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
} }

298
postgres/ynab-import.go Normal file
View File

@ -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
}