Merge pull request 'Implement Export in YNAB-Format' (#15) from ynab-export into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #15
This commit is contained in:
commit
e98e0d4779
@ -13,6 +13,7 @@ linters:
|
|||||||
- exhaustivestruct
|
- exhaustivestruct
|
||||||
- gci # not working, shows errors on freshly formatted file
|
- gci # not working, shows errors on freshly formatted file
|
||||||
- varnamelen
|
- varnamelen
|
||||||
|
- lll
|
||||||
linters-settings:
|
linters-settings:
|
||||||
errcheck:
|
errcheck:
|
||||||
exclude-functions:
|
exclude-functions:
|
||||||
|
@ -53,6 +53,49 @@ func (q *Queries) DeleteAllAssignments(ctx context.Context, budgetID uuid.UUID)
|
|||||||
return result.RowsAffected()
|
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
|
const getAssignmentsByMonthAndCategory = `-- name: GetAssignmentsByMonthAndCategory :many
|
||||||
SELECT date, category_id, budget_id, amount
|
SELECT date, category_id, budget_id, amount
|
||||||
FROM assignments_by_month
|
FROM assignments_by_month
|
||||||
|
@ -92,8 +92,45 @@ func (n Numeric) Add(other Numeric) Numeric {
|
|||||||
panic("Cannot add with different exponents")
|
panic("Cannot add with different exponents")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n Numeric) String() string {
|
||||||
|
if n.Int == nil || 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) {
|
func (n Numeric) MarshalJSON() ([]byte, error) {
|
||||||
if n.Int.Int64() == 0 {
|
if n.Int == nil || n.Int.Int64() == 0 {
|
||||||
return []byte("0"), nil
|
return []byte("0"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,4 +15,11 @@ WHERE categories.id = assignments.category_id AND category_groups.budget_id = @b
|
|||||||
-- name: GetAssignmentsByMonthAndCategory :many
|
-- name: GetAssignmentsByMonthAndCategory :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM assignments_by_month
|
FROM assignments_by_month
|
||||||
WHERE assignments_by_month.budget_id = @budget_id;
|
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;
|
@ -22,7 +22,7 @@ WHERE id = $7;
|
|||||||
DELETE FROM transactions
|
DELETE FROM transactions
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: GetTransactionsForBudget :many
|
-- name: GetAllTransactionsForBudget :many
|
||||||
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status,
|
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
|
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category
|
||||||
FROM transactions
|
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 categories ON categories.id = transactions.category_id
|
||||||
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
|
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
|
||||||
WHERE accounts.budget_id = $1
|
WHERE accounts.budget_id = $1
|
||||||
ORDER BY transactions.date DESC
|
ORDER BY transactions.date DESC;
|
||||||
LIMIT 200;
|
|
||||||
|
|
||||||
-- name: GetTransactionsForAccount :many
|
-- name: GetTransactionsForAccount :many
|
||||||
SELECT transactions.id, transactions.date, transactions.memo,
|
SELECT transactions.id, transactions.date, transactions.memo,
|
||||||
|
@ -79,6 +79,65 @@ func (q *Queries) DeleteTransaction(ctx context.Context, id uuid.UUID) error {
|
|||||||
return err
|
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
|
const getTransaction = `-- name: GetTransaction :one
|
||||||
SELECT id, date, memo, amount, account_id, category_id, payee_id, group_id, status FROM transactions
|
SELECT id, date, memo, amount, account_id, category_id, payee_id, group_id, status FROM transactions
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
@ -208,66 +267,6 @@ func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.
|
|||||||
return items, nil
|
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
|
const updateTransaction = `-- name: UpdateTransaction :exec
|
||||||
UPDATE transactions
|
UPDATE transactions
|
||||||
SET date = $1,
|
SET date = $1,
|
||||||
|
138
postgres/ynab-export.go
Normal file
138
postgres/ynab-export.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
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() + "€",
|
||||||
|
NewZeroNumeric().String() + "€",
|
||||||
|
NewZeroNumeric().String() + "€",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := csv.Write(row)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write assignment: %w", err)
|
||||||
|
}
|
||||||
|
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.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 := GetTransactionRow(transaction)
|
||||||
|
|
||||||
|
err := csv.Write(row)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write transaction: %w", err)
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
csv.Flush()
|
||||||
|
|
||||||
|
fmt.Printf("Exported %d transactions\n", count)
|
||||||
|
|
||||||
|
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, NewZeroNumeric().String()+"€", transaction.Amount.String()+"€")
|
||||||
|
} else {
|
||||||
|
row = append(row, transaction.Amount.String()[1:]+"€", NewZeroNumeric().String()+"€")
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(row, string(transaction.Status))
|
||||||
|
}
|
@ -116,7 +116,9 @@ type Transfer struct {
|
|||||||
ToAccount string
|
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 {
|
func (ynab *YNABImport) ImportTransactions(context context.Context, r io.Reader) error {
|
||||||
csv := csv.NewReader(r)
|
csv := csv.NewReader(r)
|
||||||
csv.Comma = '\t'
|
csv.Comma = '\t'
|
||||||
@ -301,27 +303,38 @@ func trimLastChar(s string) string {
|
|||||||
return s[:len(s)-size]
|
return s[:len(s)-size]
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAmount(inflow string, outflow string) (Numeric, error) {
|
func ParseNumeric(text string) (Numeric, error) {
|
||||||
// Remove trailing currency
|
// Remove trailing currency
|
||||||
inflow = strings.Replace(trimLastChar(inflow), ",", ".", 1)
|
text = trimLastChar(text)
|
||||||
outflow = strings.Replace(trimLastChar(outflow), ",", ".", 1)
|
|
||||||
|
// Unify decimal separator
|
||||||
|
text = strings.Replace(text, ",", ".", 1)
|
||||||
|
|
||||||
num := Numeric{}
|
num := Numeric{}
|
||||||
err := num.Set(inflow)
|
err := num.Set(text)
|
||||||
if err != nil {
|
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 inflow is zero, use outflow
|
||||||
if num.Int.Int64() != 0 {
|
out, err := ParseNumeric("-" + outflow)
|
||||||
return num, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = num.Set("-" + outflow)
|
|
||||||
if err != nil {
|
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) {
|
func (ynab *YNABImport) GetAccount(context context.Context, name string) (*Account, error) {
|
||||||
|
@ -67,6 +67,8 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
|
|||||||
authenticated.GET("/budget/:budgetid/autocomplete/categories", h.autocompleteCategories)
|
authenticated.GET("/budget/:budgetid/autocomplete/categories", h.autocompleteCategories)
|
||||||
authenticated.DELETE("/budget/:budgetid", h.deleteBudget)
|
authenticated.DELETE("/budget/:budgetid", h.deleteBudget)
|
||||||
authenticated.POST("/budget/:budgetid/import/ynab", h.importYNAB)
|
authenticated.POST("/budget/:budgetid/import/ynab", h.importYNAB)
|
||||||
|
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)
|
authenticated.POST("/budget/:budgetid/settings/clear", h.clearBudget)
|
||||||
|
|
||||||
budget := authenticated.Group("/budget")
|
budget := authenticated.Group("/budget")
|
||||||
|
@ -63,3 +63,55 @@ func (h *Handler) importYNAB(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) exportYNABTransactions(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user