299 lines
7.4 KiB
Go
299 lines
7.4 KiB
Go
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(context context.Context, q *Queries, budgetID uuid.UUID) (*YNABImport, error) {
|
|
accounts, err := q.GetAccounts(context, budgetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payees, err := q.GetPayees(context, budgetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
categories, err := q.GetCategories(context, budgetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
categoryGroups, err := q.GetCategoryGroups(context, budgetID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &YNABImport{
|
|
Context: context,
|
|
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
|
|
}
|