From 2adb70fa01f904d0dcb31ad8f8cd122cbf8e63ce Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 19:21:29 +0000 Subject: [PATCH 01/10] Fix comment and add csv example --- postgres/ynab-import.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/postgres/ynab-import.go b/postgres/ynab-import.go index 8aa512b..fd5f173 100644 --- a/postgres/ynab-import.go +++ b/postgres/ynab-import.go @@ -116,7 +116,9 @@ type Transfer struct { ToAccount string } -// ImportTransactions expects a TSV-file as exported by YNAB. +// ImportTransactions expects a TSV-file as exported by YNAB in the following format: +// "Account" "Flag" "Date" "Payee" "Category Group/Category" "Category Group" "Category" "Memo" "Outflow" "Inflow" "Cleared" +// "Cash" "" "11.12.2021" "Transfer : Checking" "" "" "" "Brought to bank" 500,00€ 0,00€ "Cleared" func (ynab *YNABImport) ImportTransactions(context context.Context, r io.Reader) error { csv := csv.NewReader(r) csv.Comma = '\t' From 4c7c61e8200faad28bd7deb3664d92a893ed4146 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 19:21:56 +0000 Subject: [PATCH 02/10] Add string method to numeric --- postgres/numeric.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/postgres/numeric.go b/postgres/numeric.go index f750a7b..cf8dea4 100644 --- a/postgres/numeric.go +++ b/postgres/numeric.go @@ -92,6 +92,43 @@ func (n Numeric) Add(other Numeric) Numeric { panic("Cannot add with different exponents") } +func (n Numeric) String() string { + if n.Int.Int64() == 0 { + return "0" + } + + s := fmt.Sprintf("%d", n.Int) + bytes := []byte(s) + + exp := n.Exp + for exp > 0 { + bytes = append(bytes, byte('0')) + exp-- + } + + if exp == 0 { + return string(bytes) + } + + length := int32(len(bytes)) + var bytesWithSeparator []byte + + exp = -exp + for length <= exp { + bytes = append(bytes, byte('0')) + length++ + } + + split := length - exp + bytesWithSeparator = append(bytesWithSeparator, bytes[:split]...) + if split == 1 && n.Int.Int64() < 0 { + bytesWithSeparator = append(bytesWithSeparator, byte('0')) + } + bytesWithSeparator = append(bytesWithSeparator, byte('.')) + bytesWithSeparator = append(bytesWithSeparator, bytes[split:]...) + return string(bytesWithSeparator) +} + func (n Numeric) MarshalJSON() ([]byte, error) { if n.Int.Int64() == 0 { return []byte("0"), nil From 27188e2e276e0a2eb70f57b9c92e117c31cbe9dc Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 19:32:49 +0000 Subject: [PATCH 03/10] Implement ynab-export --- postgres/assignments.sql.go | 43 +++++++++++++ postgres/queries/assignments.sql | 9 ++- postgres/ynab-export.go | 102 +++++++++++++++++++++++++++++++ server/http.go | 1 + server/ynab-import.go | 32 ++++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 postgres/ynab-export.go diff --git a/postgres/assignments.sql.go b/postgres/assignments.sql.go index 5398652..0689a18 100644 --- a/postgres/assignments.sql.go +++ b/postgres/assignments.sql.go @@ -53,6 +53,49 @@ func (q *Queries) DeleteAllAssignments(ctx context.Context, budgetID uuid.UUID) return result.RowsAffected() } +const getAllAssignments = `-- name: GetAllAssignments :many +SELECT assignments.date, categories.name as category, category_groups.name as group, assignments.amount +FROM assignments +INNER JOIN categories ON categories.id = assignments.category_id +INNER JOIN category_groups ON categories.category_group_id = category_groups.id +WHERE category_groups.budget_id = $1 +` + +type GetAllAssignmentsRow struct { + Date time.Time + Category string + Group string + Amount Numeric +} + +func (q *Queries) GetAllAssignments(ctx context.Context, budgetID uuid.UUID) ([]GetAllAssignmentsRow, error) { + rows, err := q.db.QueryContext(ctx, getAllAssignments, budgetID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllAssignmentsRow + for rows.Next() { + var i GetAllAssignmentsRow + if err := rows.Scan( + &i.Date, + &i.Category, + &i.Group, + &i.Amount, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getAssignmentsByMonthAndCategory = `-- name: GetAssignmentsByMonthAndCategory :many SELECT date, category_id, budget_id, amount FROM assignments_by_month diff --git a/postgres/queries/assignments.sql b/postgres/queries/assignments.sql index 1cffd54..56975ef 100644 --- a/postgres/queries/assignments.sql +++ b/postgres/queries/assignments.sql @@ -15,4 +15,11 @@ WHERE categories.id = assignments.category_id AND category_groups.budget_id = @b -- name: GetAssignmentsByMonthAndCategory :many SELECT * FROM assignments_by_month -WHERE assignments_by_month.budget_id = @budget_id; \ No newline at end of file +WHERE assignments_by_month.budget_id = @budget_id; + +-- name: GetAllAssignments :many +SELECT assignments.date, categories.name as category, category_groups.name as group, assignments.amount +FROM assignments +INNER JOIN categories ON categories.id = assignments.category_id +INNER JOIN category_groups ON categories.category_group_id = category_groups.id +WHERE category_groups.budget_id = @budget_id; \ No newline at end of file diff --git a/postgres/ynab-export.go b/postgres/ynab-export.go new file mode 100644 index 0000000..0443929 --- /dev/null +++ b/postgres/ynab-export.go @@ -0,0 +1,102 @@ +package postgres + +import ( + "context" + "encoding/csv" + "fmt" + "io" + + "github.com/google/uuid" +) + +type YNABExport struct { + queries *Queries + budgetID uuid.UUID +} + +func NewYNABExport(context context.Context, queries *Queries, budgetID uuid.UUID) (*YNABExport, error) { + return &YNABExport{ + queries: queries, + budgetID: budgetID, + }, nil +} + +// ImportAssignments expects a TSV-file as exported by YNAB in the following format: +// "Month" "Category Group/Category" "Category Group" "Category" "Budgeted" "Activity" "Available" +// "Apr 2019" "Income: Next Month" "Income" "Next Month" 0,00€ 0,00€ 0,00€ +// +// Activity and Available are not imported, since they are determined by the transactions and historic assignments. +func (ynab *YNABExport) ExportAssignments(context context.Context, w io.Writer) error { + csv := csv.NewWriter(w) + csv.Comma = '\t' + + assignments, err := ynab.queries.GetAllAssignments(context, ynab.budgetID) + if err != nil { + return fmt.Errorf("load assignments: %w", err) + } + + count := 0 + for _, assignment := range assignments { + row := []string{ + assignment.Date.Format("Jan 2006"), + assignment.Group + ": " + assignment.Category, + assignment.Group, + assignment.Category, + assignment.Amount.String() + "€", + "0,00€", + "0,00€", + } + + csv.Write(row) + count++ + } + csv.Flush() + + fmt.Printf("Exported %d assignments\n", count) + + return nil +} + +// ImportTransactions expects a TSV-file as exported by YNAB in the following format: +// "Account" "Flag" "Date" "Payee" "Category Group/Category" "Category Group" "Category" "Memo" "Outflow" "Inflow" "Cleared" +// "Cash" "" "11.12.2021" "Transfer : Checking" "" "" "" "Brought to bank" 500,00€ 0,00€ "Cleared" +func (ynab *YNABExport) ExportTransactions(context context.Context, w io.Writer) error { + csv := csv.NewWriter(w) + csv.Comma = '\t' + + transactions, err := ynab.queries.GetTransactionsForBudget(context, ynab.budgetID) + if err != nil { + return fmt.Errorf("load transactions: %w", err) + } + + count := 0 + for _, transaction := range transactions { + row := []string{ + transaction.Account, + "", // Flag + transaction.Date.Format("02.01.2006"), + transaction.Payee, + transaction.CategoryGroup + " : " + transaction.Category, + transaction.CategoryGroup, + transaction.Category, + transaction.Memo, + } + + if transaction.Amount.IsPositive() { + row = append(row, transaction.Amount.String()+"€", "0,00€") + } else { + row = append(row, "0,00€", transaction.Amount.String()[1:]+"€") + } + + row = append(row, string(transaction.Status)) + + csv.Write(row) + count++ + } + + csv.Flush() + + fmt.Printf("Exported %d transactions\n", count) + + return nil +} diff --git a/server/http.go b/server/http.go index 569464b..dbc623f 100644 --- a/server/http.go +++ b/server/http.go @@ -67,6 +67,7 @@ func (h *Handler) LoadRoutes(router *gin.Engine) { authenticated.GET("/budget/:budgetid/autocomplete/categories", h.autocompleteCategories) authenticated.DELETE("/budget/:budgetid", h.deleteBudget) authenticated.POST("/budget/:budgetid/import/ynab", h.importYNAB) + authenticated.POST("/budget/:budgetid/export/ynab", h.exportYNAB) authenticated.POST("/budget/:budgetid/settings/clear", h.clearBudget) budget := authenticated.Group("/budget") diff --git a/server/ynab-import.go b/server/ynab-import.go index 349449e..11d0a4c 100644 --- a/server/ynab-import.go +++ b/server/ynab-import.go @@ -63,3 +63,35 @@ func (h *Handler) importYNAB(c *gin.Context) { return } } + +func (h *Handler) exportYNAB(c *gin.Context) { + budgetID, succ := c.Params.Get("budgetid") + if !succ { + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"no budget_id specified"}) + return + } + + budgetUUID, err := uuid.Parse(budgetID) + if !succ { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + ynab, err := postgres.NewYNABExport(c.Request.Context(), h.Service.Queries, budgetUUID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + err = ynab.ExportTransactions(c.Request.Context(), c.Writer) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + err = ynab.ExportAssignments(c.Request.Context(), c.Writer) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } +} From ece610419fdd64005d41a083f5f4994ea3dcdc1b Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 19:37:08 +0000 Subject: [PATCH 04/10] Disable linter lll because no exceptions in comments are possible --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index c8a3043..81fc978 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,6 +13,7 @@ linters: - exhaustivestruct - gci # not working, shows errors on freshly formatted file - varnamelen + - lll linters-settings: errcheck: exclude-functions: From a7cd3512bbf508e375ac875fca42c908cb14f006 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 19:37:32 +0000 Subject: [PATCH 05/10] Handle errors of Write and add dots to comments --- postgres/ynab-export.go | 12 +++++++++--- postgres/ynab-import.go | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/postgres/ynab-export.go b/postgres/ynab-export.go index 0443929..95e5290 100644 --- a/postgres/ynab-export.go +++ b/postgres/ynab-export.go @@ -47,7 +47,10 @@ func (ynab *YNABExport) ExportAssignments(context context.Context, w io.Writer) "0,00€", } - csv.Write(row) + err := csv.Write(row) + if err != nil { + return fmt.Errorf("write assignment: %w", err) + } count++ } csv.Flush() @@ -59,7 +62,7 @@ func (ynab *YNABExport) ExportAssignments(context context.Context, w io.Writer) // ImportTransactions expects a TSV-file as exported by YNAB in the following format: // "Account" "Flag" "Date" "Payee" "Category Group/Category" "Category Group" "Category" "Memo" "Outflow" "Inflow" "Cleared" -// "Cash" "" "11.12.2021" "Transfer : Checking" "" "" "" "Brought to bank" 500,00€ 0,00€ "Cleared" +// "Cash" "" "11.12.2021" "Transfer : Checking" "" "" "" "Brought to bank" 500,00€ 0,00€ "Cleared". func (ynab *YNABExport) ExportTransactions(context context.Context, w io.Writer) error { csv := csv.NewWriter(w) csv.Comma = '\t' @@ -90,7 +93,10 @@ func (ynab *YNABExport) ExportTransactions(context context.Context, w io.Writer) row = append(row, string(transaction.Status)) - csv.Write(row) + err := csv.Write(row) + if err != nil { + return fmt.Errorf("write transaction: %w", err) + } count++ } diff --git a/postgres/ynab-import.go b/postgres/ynab-import.go index fd5f173..f54d092 100644 --- a/postgres/ynab-import.go +++ b/postgres/ynab-import.go @@ -118,7 +118,7 @@ type Transfer struct { // ImportTransactions expects a TSV-file as exported by YNAB in the following format: // "Account" "Flag" "Date" "Payee" "Category Group/Category" "Category Group" "Category" "Memo" "Outflow" "Inflow" "Cleared" -// "Cash" "" "11.12.2021" "Transfer : Checking" "" "" "" "Brought to bank" 500,00€ 0,00€ "Cleared" +// "Cash" "" "11.12.2021" "Transfer : Checking" "" "" "" "Brought to bank" 500,00€ 0,00€ "Cleared". func (ynab *YNABImport) ImportTransactions(context context.Context, r io.Reader) error { csv := csv.NewReader(r) csv.Comma = '\t' From e0dc7800af075e73c8cf37ad21c33504b579ff95 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 20:50:29 +0000 Subject: [PATCH 06/10] Remove limit for GetAllTransactionsForBudget --- postgres/queries/transactions.sql | 5 +- postgres/transactions.sql.go | 119 +++++++++++++++--------------- 2 files changed, 61 insertions(+), 63 deletions(-) diff --git a/postgres/queries/transactions.sql b/postgres/queries/transactions.sql index 65677cd..291b357 100644 --- a/postgres/queries/transactions.sql +++ b/postgres/queries/transactions.sql @@ -22,7 +22,7 @@ WHERE id = $7; DELETE FROM transactions WHERE id = $1; --- name: GetTransactionsForBudget :many +-- name: GetAllTransactionsForBudget :many SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status, accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category FROM transactions @@ -31,8 +31,7 @@ LEFT JOIN payees ON payees.id = transactions.payee_id LEFT JOIN categories ON categories.id = transactions.category_id LEFT JOIN category_groups ON category_groups.id = categories.category_group_id WHERE accounts.budget_id = $1 -ORDER BY transactions.date DESC -LIMIT 200; +ORDER BY transactions.date DESC; -- name: GetTransactionsForAccount :many SELECT transactions.id, transactions.date, transactions.memo, diff --git a/postgres/transactions.sql.go b/postgres/transactions.sql.go index e6b54d7..f5c813d 100644 --- a/postgres/transactions.sql.go +++ b/postgres/transactions.sql.go @@ -79,6 +79,65 @@ func (q *Queries) DeleteTransaction(ctx context.Context, id uuid.UUID) error { return err } +const getAllTransactionsForBudget = `-- name: GetAllTransactionsForBudget :many +SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status, + accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category +FROM transactions +INNER JOIN accounts ON accounts.id = transactions.account_id +LEFT JOIN payees ON payees.id = transactions.payee_id +LEFT JOIN categories ON categories.id = transactions.category_id +LEFT JOIN category_groups ON category_groups.id = categories.category_group_id +WHERE accounts.budget_id = $1 +ORDER BY transactions.date DESC +` + +type GetAllTransactionsForBudgetRow struct { + ID uuid.UUID + Date time.Time + Memo string + Amount Numeric + GroupID uuid.NullUUID + Status TransactionStatus + Account string + Payee string + CategoryGroup string + Category string +} + +func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid.UUID) ([]GetAllTransactionsForBudgetRow, error) { + rows, err := q.db.QueryContext(ctx, getAllTransactionsForBudget, budgetID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllTransactionsForBudgetRow + for rows.Next() { + var i GetAllTransactionsForBudgetRow + if err := rows.Scan( + &i.ID, + &i.Date, + &i.Memo, + &i.Amount, + &i.GroupID, + &i.Status, + &i.Account, + &i.Payee, + &i.CategoryGroup, + &i.Category, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getTransaction = `-- name: GetTransaction :one SELECT id, date, memo, amount, account_id, category_id, payee_id, group_id, status FROM transactions WHERE id = $1 @@ -208,66 +267,6 @@ func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid. return items, nil } -const getTransactionsForBudget = `-- name: GetTransactionsForBudget :many -SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status, - accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category -FROM transactions -INNER JOIN accounts ON accounts.id = transactions.account_id -LEFT JOIN payees ON payees.id = transactions.payee_id -LEFT JOIN categories ON categories.id = transactions.category_id -LEFT JOIN category_groups ON category_groups.id = categories.category_group_id -WHERE accounts.budget_id = $1 -ORDER BY transactions.date DESC -LIMIT 200 -` - -type GetTransactionsForBudgetRow struct { - ID uuid.UUID - Date time.Time - Memo string - Amount Numeric - GroupID uuid.NullUUID - Status TransactionStatus - Account string - Payee string - CategoryGroup string - Category string -} - -func (q *Queries) GetTransactionsForBudget(ctx context.Context, budgetID uuid.UUID) ([]GetTransactionsForBudgetRow, error) { - rows, err := q.db.QueryContext(ctx, getTransactionsForBudget, budgetID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetTransactionsForBudgetRow - for rows.Next() { - var i GetTransactionsForBudgetRow - if err := rows.Scan( - &i.ID, - &i.Date, - &i.Memo, - &i.Amount, - &i.GroupID, - &i.Status, - &i.Account, - &i.Payee, - &i.CategoryGroup, - &i.Category, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const updateTransaction = `-- name: UpdateTransaction :exec UPDATE transactions SET date = $1, From bc65249c03704dd7e88a33a6f7427ffdda7bb75f Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 20:50:59 +0000 Subject: [PATCH 07/10] Split transactions and assignments export into two endpoints --- postgres/ynab-export.go | 68 +++++++++++++++++++++++++++++------------ server/http.go | 3 +- server/ynab-import.go | 22 ++++++++++++- 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/postgres/ynab-export.go b/postgres/ynab-export.go index 95e5290..636b455 100644 --- a/postgres/ynab-export.go +++ b/postgres/ynab-export.go @@ -67,31 +67,33 @@ func (ynab *YNABExport) ExportTransactions(context context.Context, w io.Writer) csv := csv.NewWriter(w) csv.Comma = '\t' - transactions, err := ynab.queries.GetTransactionsForBudget(context, ynab.budgetID) + transactions, err := ynab.queries.GetAllTransactionsForBudget(context, ynab.budgetID) if err != nil { return fmt.Errorf("load transactions: %w", err) } + header := []string{ + "Account", + "Flag", + "Date", + "Payee", + "Category Group/Category", + "Category Group", + "Category", + "Memo", + "Outflow", + "Inflow", + "Cleared", + } + + err = csv.Write(header) + if err != nil { + return fmt.Errorf("write transaction: %w", err) + } + count := 0 for _, transaction := range transactions { - row := []string{ - transaction.Account, - "", // Flag - transaction.Date.Format("02.01.2006"), - transaction.Payee, - transaction.CategoryGroup + " : " + transaction.Category, - transaction.CategoryGroup, - transaction.Category, - transaction.Memo, - } - - if transaction.Amount.IsPositive() { - row = append(row, transaction.Amount.String()+"€", "0,00€") - } else { - row = append(row, "0,00€", transaction.Amount.String()[1:]+"€") - } - - row = append(row, string(transaction.Status)) + row := GetTransactionRow(transaction) err := csv.Write(row) if err != nil { @@ -106,3 +108,31 @@ func (ynab *YNABExport) ExportTransactions(context context.Context, w io.Writer) return nil } + +func GetTransactionRow(transaction GetAllTransactionsForBudgetRow) []string { + row := []string{ + transaction.Account, + "", // Flag + transaction.Date.Format("02.01.2006"), + transaction.Payee, + } + + if transaction.CategoryGroup != "" && transaction.Category != "" { + row = append(row, + transaction.CategoryGroup+" : "+transaction.Category, + transaction.CategoryGroup, + transaction.Category) + } else { + row = append(row, "", "", "") + } + + row = append(row, transaction.Memo) + + if transaction.Amount.IsPositive() { + row = append(row, transaction.Amount.String()+"€", "0,00€") + } else { + row = append(row, "0,00€", transaction.Amount.String()[1:]+"€") + } + + return append(row, string(transaction.Status)) +} diff --git a/server/http.go b/server/http.go index dbc623f..a80f55c 100644 --- a/server/http.go +++ b/server/http.go @@ -67,7 +67,8 @@ func (h *Handler) LoadRoutes(router *gin.Engine) { authenticated.GET("/budget/:budgetid/autocomplete/categories", h.autocompleteCategories) authenticated.DELETE("/budget/:budgetid", h.deleteBudget) authenticated.POST("/budget/:budgetid/import/ynab", h.importYNAB) - authenticated.POST("/budget/:budgetid/export/ynab", h.exportYNAB) + authenticated.POST("/budget/:budgetid/export/ynab/transactions", h.exportYNABTransactions) + authenticated.POST("/budget/:budgetid/export/ynab/assignments", h.exportYNABAssignments) authenticated.POST("/budget/:budgetid/settings/clear", h.clearBudget) budget := authenticated.Group("/budget") diff --git a/server/ynab-import.go b/server/ynab-import.go index 11d0a4c..ac0da74 100644 --- a/server/ynab-import.go +++ b/server/ynab-import.go @@ -64,7 +64,7 @@ func (h *Handler) importYNAB(c *gin.Context) { } } -func (h *Handler) exportYNAB(c *gin.Context) { +func (h *Handler) exportYNABTransactions(c *gin.Context) { budgetID, succ := c.Params.Get("budgetid") if !succ { c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"no budget_id specified"}) @@ -88,6 +88,26 @@ func (h *Handler) exportYNAB(c *gin.Context) { c.AbortWithError(http.StatusInternalServerError, err) return } +} + +func (h *Handler) exportYNABAssignments(c *gin.Context) { + budgetID, succ := c.Params.Get("budgetid") + if !succ { + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"no budget_id specified"}) + return + } + + budgetUUID, err := uuid.Parse(budgetID) + if !succ { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + ynab, err := postgres.NewYNABExport(c.Request.Context(), h.Service.Queries, budgetUUID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } err = ynab.ExportAssignments(c.Request.Context(), c.Writer) if err != nil { From 34b6e450de8a3edf9a8b80136101aaa4741b9409 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 21:09:25 +0000 Subject: [PATCH 08/10] Handle nil in Numeric --- postgres/numeric.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postgres/numeric.go b/postgres/numeric.go index cf8dea4..9483e87 100644 --- a/postgres/numeric.go +++ b/postgres/numeric.go @@ -93,7 +93,7 @@ func (n Numeric) Add(other Numeric) Numeric { } func (n Numeric) String() string { - if n.Int.Int64() == 0 { + if n.Int == nil || n.Int.Int64() == 0 { return "0" } @@ -130,7 +130,7 @@ func (n Numeric) String() string { } func (n Numeric) MarshalJSON() ([]byte, error) { - if n.Int.Int64() == 0 { + if n.Int == nil || n.Int.Int64() == 0 { return []byte("0"), nil } From 0478d82c1faf246ea6bd8d44c154dc00e8a6e056 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 21:09:33 +0000 Subject: [PATCH 09/10] Fix order of fields --- postgres/ynab-export.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postgres/ynab-export.go b/postgres/ynab-export.go index 636b455..f003dc4 100644 --- a/postgres/ynab-export.go +++ b/postgres/ynab-export.go @@ -129,9 +129,9 @@ func GetTransactionRow(transaction GetAllTransactionsForBudgetRow) []string { row = append(row, transaction.Memo) if transaction.Amount.IsPositive() { - row = append(row, transaction.Amount.String()+"€", "0,00€") + row = append(row, "0,00€", transaction.Amount.String()+"€") } else { - row = append(row, "0,00€", transaction.Amount.String()[1:]+"€") + row = append(row, transaction.Amount.String()[1:]+"€", "0,00€") } return append(row, string(transaction.Status)) From 6686904539568451510c11ac5d654bc2afe282ea Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Wed, 23 Feb 2022 21:17:43 +0000 Subject: [PATCH 10/10] Use zero Numeric for export instead of hardcoding 0,00 --- postgres/ynab-export.go | 8 ++++---- postgres/ynab-import.go | 35 +++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/postgres/ynab-export.go b/postgres/ynab-export.go index f003dc4..2fa1a4c 100644 --- a/postgres/ynab-export.go +++ b/postgres/ynab-export.go @@ -43,8 +43,8 @@ func (ynab *YNABExport) ExportAssignments(context context.Context, w io.Writer) assignment.Group, assignment.Category, assignment.Amount.String() + "€", - "0,00€", - "0,00€", + NewZeroNumeric().String() + "€", + NewZeroNumeric().String() + "€", } err := csv.Write(row) @@ -129,9 +129,9 @@ func GetTransactionRow(transaction GetAllTransactionsForBudgetRow) []string { row = append(row, transaction.Memo) if transaction.Amount.IsPositive() { - row = append(row, "0,00€", transaction.Amount.String()+"€") + row = append(row, NewZeroNumeric().String()+"€", transaction.Amount.String()+"€") } else { - row = append(row, transaction.Amount.String()[1:]+"€", "0,00€") + row = append(row, transaction.Amount.String()[1:]+"€", NewZeroNumeric().String()+"€") } return append(row, string(transaction.Status)) diff --git a/postgres/ynab-import.go b/postgres/ynab-import.go index f54d092..629a844 100644 --- a/postgres/ynab-import.go +++ b/postgres/ynab-import.go @@ -303,27 +303,38 @@ func trimLastChar(s string) string { return s[:len(s)-size] } -func GetAmount(inflow string, outflow string) (Numeric, error) { +func ParseNumeric(text string) (Numeric, error) { // Remove trailing currency - inflow = strings.Replace(trimLastChar(inflow), ",", ".", 1) - outflow = strings.Replace(trimLastChar(outflow), ",", ".", 1) + text = trimLastChar(text) + + // Unify decimal separator + text = strings.Replace(text, ",", ".", 1) num := Numeric{} - err := num.Set(inflow) + err := num.Set(text) if err != nil { - return num, fmt.Errorf("parse inflow %s: %w", inflow, err) + return num, fmt.Errorf("parse numeric %s: %w", text, err) + } + + return num, nil +} + +func GetAmount(inflow string, outflow string) (Numeric, error) { + in, err := ParseNumeric(inflow) + if err != nil { + return in, err + } + + if !in.IsZero() { + return in, nil } // if inflow is zero, use outflow - if num.Int.Int64() != 0 { - return num, nil - } - - err = num.Set("-" + outflow) + out, err := ParseNumeric("-" + outflow) if err != nil { - return num, fmt.Errorf("parse outflow %s: %w", inflow, err) + return out, err } - return num, nil + return out, nil } func (ynab *YNABImport) GetAccount(context context.Context, name string) (*Account, error) {