Skip to content

Commit

Permalink
Merge pull request #57 from martinohansen/martin/reader-writer-interf…
Browse files Browse the repository at this point in the history
…aces

Martin/reader writer interfaces
  • Loading branch information
martinohansen committed Dec 22, 2023
2 parents 05f8d35 + bbd551b commit 049b7aa
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 55 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
go-version: 1.21

- name: Build
run: go build -v ./...
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ you have problems with a specific bank.

[^1]: Set NORDIGEN_TRANSACTION_ID to "InternalTransactionId" if using YNAB_IMPORT_ID_V2

## Writers

The default writer is YNAB (that's really what this tool is set out to handle)
but we also have a JSON writer that can be used for testing purposes.

| Writer | Description |
|---------|---------------|
| YNAB | Pushes transactions to YNAB |
| JSON | Writes transactions to stdout in JSON format |

## Contributing

Pull requests are welcome.
53 changes: 34 additions & 19 deletions cmd/ynabber/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/kelseyhightower/envconfig"
"github.com/martinohansen/ynabber"
"github.com/martinohansen/ynabber/reader/nordigen"
"github.com/martinohansen/ynabber/writer/json"
"github.com/martinohansen/ynabber/writer/ynab"
)

Expand Down Expand Up @@ -53,8 +54,28 @@ func main() {
log.Printf("Config: %+v\n", cfg)
}

ynabber := ynabber.Ynabber{}
for _, reader := range cfg.Readers {
switch reader {
case "nordigen":
ynabber.Readers = append(ynabber.Readers, nordigen.Reader{Config: &cfg})
default:
log.Fatalf("Unknown reader: %s", reader)
}
}
for _, writer := range cfg.Writers {
switch writer {
case "ynab":
ynabber.Writers = append(ynabber.Writers, ynab.Writer{Config: &cfg})
case "json":
ynabber.Writers = append(ynabber.Writers, json.Writer{})
default:
log.Fatalf("Unknown writer: %s", writer)
}
}

for {
err = run(cfg)
err = run(ynabber, cfg.Interval)
if err != nil {
panic(err)
} else {
Expand All @@ -69,29 +90,23 @@ func main() {
}
}

func run(cfg ynabber.Config) error {
func run(y ynabber.Ynabber, interval time.Duration) error {
var transactions []ynabber.Transaction

for _, reader := range cfg.Readers {
log.Printf("Reading from %s", reader)
switch reader {
case "nordigen":
t, err := nordigen.BulkReader(cfg)
if err != nil {
return fmt.Errorf("couldn't read from nordigen: %w", err)
}
transactions = append(transactions, t...)
// Read transactions from all readers
for _, reader := range y.Readers {
t, err := reader.Bulk()
if err != nil {
return fmt.Errorf("reading: %w", err)
}
transactions = append(transactions, t...)
}

for _, writer := range cfg.Writers {
log.Printf("Writing to %s", writer)
switch writer {
case "ynab":
err := ynab.BulkWriter(cfg, transactions)
if err != nil {
return fmt.Errorf("couldn't write to ynab: %w", err)
}
// Write transactions to all writers
for _, writer := range y.Writers {
err := writer.Bulk(transactions)
if err != nil {
return fmt.Errorf("writing: %w", err)
}
}
return nil
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/martinohansen/ynabber

go 1.17
go 1.21

require github.com/frieser/nordigen-go-lib/v2 v2.1.4

Expand Down
13 changes: 7 additions & 6 deletions reader/nordigen/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/martinohansen/ynabber"
"log"
"os"
"os/exec"
"path"
"strconv"
"time"

"github.com/martinohansen/ynabber"

"github.com/frieser/nordigen-go-lib/v2"
)

Expand All @@ -28,11 +29,11 @@ func (auth Authorization) Store() string {

// AuthorizationWrapper tries to get requisition from disk, if it fails it will
// create a new and store that one to disk.
func (auth Authorization) Wrapper(cfg ynabber.Config) (nordigen.Requisition, error) {
func (auth Authorization) Wrapper(cfg *ynabber.Config) (nordigen.Requisition, error) {
requisitionFile, err := os.ReadFile(auth.Store())
if errors.Is(err, os.ErrNotExist) {
log.Print("Requisition is not found")
return auth.CreateAndSave(cfg)
return auth.CreateAndSave(*cfg)
} else if err != nil {
return nordigen.Requisition{}, fmt.Errorf("ReadFile: %w", err)
}
Expand All @@ -41,18 +42,18 @@ func (auth Authorization) Wrapper(cfg ynabber.Config) (nordigen.Requisition, err
err = json.Unmarshal(requisitionFile, &requisition)
if err != nil {
log.Print("Failed to parse requisition file")
return auth.CreateAndSave(cfg)
return auth.CreateAndSave(*cfg)
}

switch requisition.Status {
case "EX":
log.Printf("Requisition is expired")
return auth.CreateAndSave(cfg)
return auth.CreateAndSave(*cfg)
case "LN":
return requisition, nil
default:
log.Printf("Unsupported requisition status: %s", requisition.Status)
return auth.CreateAndSave(cfg)
return auth.CreateAndSave(*cfg)
}
}

Expand Down
24 changes: 14 additions & 10 deletions reader/nordigen/nordigen.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import (
"github.com/martinohansen/ynabber"
)

type Reader struct {
Config *ynabber.Config
}

// payeeStripNonAlphanumeric removes all non-alphanumeric characters from payee
func payeeStripNonAlphanumeric(payee string) (x string) {
reg := regexp.MustCompile(`[^\p{L}]+`)
Expand Down Expand Up @@ -81,24 +85,24 @@ func dataFile(cfg ynabber.Config) string {
return dataFile
}

func BulkReader(cfg ynabber.Config) (t []ynabber.Transaction, err error) {
c, err := nordigen.NewClient(cfg.Nordigen.SecretID, cfg.Nordigen.SecretKey)
func (r Reader) Bulk() (t []ynabber.Transaction, err error) {
c, err := nordigen.NewClient(r.Config.Nordigen.SecretID, r.Config.Nordigen.SecretKey)
if err != nil {
return nil, fmt.Errorf("failed to create client: %w", err)
}

Authorization := Authorization{
Client: *c,
BankID: cfg.Nordigen.BankID,
File: dataFile(cfg),
BankID: r.Config.Nordigen.BankID,
File: dataFile(*r.Config),
}
r, err := Authorization.Wrapper(cfg)
req, err := Authorization.Wrapper(r.Config)
if err != nil {
return nil, fmt.Errorf("failed to authorize: %w", err)
}

log.Printf("Found %v accounts", len(r.Accounts))
for _, account := range r.Accounts {
log.Printf("Found %v accounts", len(req.Accounts))
for _, account := range req.Accounts {
accountMetadata, err := c.GetAccountMetadata(account)
if err != nil {
return nil, fmt.Errorf("failed to get account metadata: %w", err)
Expand All @@ -113,7 +117,7 @@ func BulkReader(cfg ynabber.Config) (t []ynabber.Transaction, err error) {
account,
accountMetadata.Status,
)
Authorization.CreateAndSave(cfg)
Authorization.CreateAndSave(*r.Config)
}

account := ynabber.Account{
Expand All @@ -129,11 +133,11 @@ func BulkReader(cfg ynabber.Config) (t []ynabber.Transaction, err error) {
return nil, fmt.Errorf("failed to get transactions: %w", err)
}

if cfg.Debug {
if r.Config.Debug {
log.Printf("Transactions received from Nordigen: %+v", transactions)
}

x, err := transactionsToYnabber(cfg, account, transactions)
x, err := transactionsToYnabber(*r.Config, account, transactions)
if err != nil {
return nil, fmt.Errorf("failed to convert transaction: %w", err)
}
Expand Down
19 changes: 19 additions & 0 deletions writer/json/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package json

import (
"encoding/json"
"fmt"

"github.com/martinohansen/ynabber"
)

type Writer struct{}

func (w Writer) Bulk(tx []ynabber.Transaction) error {
b, err := json.MarshalIndent(tx, "", " ")
if err != nil {
return fmt.Errorf("marshalling: %w", err)
}
fmt.Println(string(b))
return nil
}
18 changes: 11 additions & 7 deletions writer/ynab/ynab.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import (
const maxMemoSize int = 200 // Max size of memo field in YNAB API
const maxPayeeSize int = 100 // Max size of payee field in YNAB API

type Writer struct {
Config *ynabber.Config
}

var space = regexp.MustCompile(`\s+`) // Matches all whitespace characters

// Ytransaction is a single YNAB transaction
Expand Down Expand Up @@ -133,7 +137,7 @@ func ynabberToYNAB(cfg ynabber.Config, t ynabber.Transaction) (Ytransaction, err
}, nil
}

func BulkWriter(cfg ynabber.Config, t []ynabber.Transaction) error {
func (w Writer) Bulk(t []ynabber.Transaction) error {
// skipped and failed counters
skipped := 0
failed := 0
Expand All @@ -142,12 +146,12 @@ func BulkWriter(cfg ynabber.Config, t []ynabber.Transaction) error {
y := new(Ytransactions)
for _, v := range t {
// Skip transaction if the date is before FromDate
if v.Date.Before(time.Time(cfg.YNAB.FromDate)) {
if v.Date.Before(time.Time(w.Config.YNAB.FromDate)) {
skipped += 1
continue
}

transaction, err := ynabberToYNAB(cfg, v)
transaction, err := ynabberToYNAB(*w.Config, v)
if err != nil {
// If we fail to parse a single transaction we log it but move on so
// we don't halt the entire program.
Expand All @@ -163,11 +167,11 @@ func BulkWriter(cfg ynabber.Config, t []ynabber.Transaction) error {
return nil
}

if cfg.Debug {
if w.Config.Debug {
log.Printf("Request to YNAB: %+v", y)
}

url := fmt.Sprintf("https://api.youneedabudget.com/v1/budgets/%s/transactions", cfg.YNAB.BudgetID)
url := fmt.Sprintf("https://api.youneedabudget.com/v1/budgets/%s/transactions", w.Config.YNAB.BudgetID)

payload, err := json.Marshal(y)
if err != nil {
Expand All @@ -181,15 +185,15 @@ func BulkWriter(cfg ynabber.Config, t []ynabber.Transaction) error {
return err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", cfg.YNAB.Token))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", w.Config.YNAB.Token))

res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()

if cfg.Debug {
if w.Config.Debug {
b, _ := httputil.DumpResponse(res, true)
log.Printf("Response from YNAB: %s", b)
}
Expand Down
30 changes: 19 additions & 11 deletions ynabber.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ import (
"time"
)

type Ynabber struct {
Readers []Reader
Writers []Writer
}

type Reader interface {
Bulk() ([]Transaction, error)
}

type Writer interface {
Bulk([]Transaction) error
}

type Account struct {
ID ID
Name string
Expand Down Expand Up @@ -33,17 +46,12 @@ func (m Milliunits) Negate() Milliunits {
}

type Transaction struct {
Account Account
ID ID
Date time.Time
Payee Payee
Memo string
Amount Milliunits
}

type Ynabber interface {
bulkReader() ([]Transaction, error)
bulkWriter([]Transaction) error
Account Account `json:"account"`
ID ID `json:"id"`
Date time.Time `json:"date"`
Payee Payee `json:"payee"`
Memo string `json:"memo"`
Amount Milliunits `json:"amount"`
}

func (m Milliunits) String() string {
Expand Down

0 comments on commit 049b7aa

Please sign in to comment.