-
Notifications
You must be signed in to change notification settings - Fork 3
/
parse.go
101 lines (82 loc) · 2.95 KB
/
parse.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package ynabimport
import (
"encoding/csv"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/envelope-zero/backend/v2/pkg/importer"
"github.com/envelope-zero/backend/v2/pkg/importer/helpers"
"github.com/envelope-zero/backend/v2/pkg/models"
"github.com/shopspring/decimal"
)
// This function parses the YNAB import CSV files.
func Parse(f io.Reader, account models.Account) ([]importer.TransactionPreview, error) {
reader := csv.NewReader(f)
// We can reuse the array in the background to improve performance
reader.ReuseRecord = true
var transactions []importer.TransactionPreview
// Skip the first line
_, err := reader.Read()
if err == io.EOF {
return []importer.TransactionPreview{}, nil
}
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return csvReadError(reader, fmt.Errorf("could not read line in CSV: %w", err))
}
date, err := time.Parse("01/02/2006", record[Date])
if err != nil {
return csvReadError(reader, fmt.Errorf("could not parse time: %w", err))
}
t := importer.TransactionPreview{
Model: models.Transaction{
TransactionCreate: models.TransactionCreate{
Date: date,
ImportHash: helpers.Sha256String(strings.Join(record, ",")),
Note: record[Memo],
BudgetID: account.BudgetID,
},
},
}
// Set the source and destination account
if record[Outflow] != "" && record[Inflow] != "" {
return csvReadError(reader, errors.New("both outflow and inflow are set for the transaction"))
} else if record[Outflow] == "" && record[Inflow] == "" {
return csvReadError(reader, errors.New("no amount is set for the transaction"))
} else if record[Outflow] != "" {
t.Model.TransactionCreate.SourceAccountID = account.DefaultModel.ID
t.DestinationAccountName = record[Payee]
amount, err := decimal.NewFromString(record[Outflow])
if err != nil {
return csvReadError(reader, errors.New("outflow could not be parsed to a decimal"))
}
t.Model.TransactionCreate.Amount = amount.Neg()
} else {
t.Model.TransactionCreate.DestinationAccountID = account.DefaultModel.ID
t.SourceAccountName = record[Payee]
amount, err := decimal.NewFromString(record[Inflow])
if err != nil {
return csvReadError(reader, errors.New("inflow could not be parsed to a decimal"))
}
t.Model.TransactionCreate.Amount = amount
}
if t.Model.TransactionCreate.Amount.IsZero() {
return csvReadError(reader, errors.New("the amount for a transaction must not be 0"))
}
transactions = append(transactions, t)
}
return transactions, nil
}
// csvReadError returns the an error with the format string, including the line of the input
// the error occurred in in the message.
func csvReadError(r *csv.Reader, err error) ([]importer.TransactionPreview, error) {
// always use the first field, we are only interested in the line
line, _ := r.FieldPos(1)
return []importer.TransactionPreview{}, fmt.Errorf("error in line %d of the CSV: %w", line, err)
}