Skip to content

Commit

Permalink
Merge pull request #58 from martinohansen/martin/reqs-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
martinohansen authored Dec 22, 2023
2 parents 049b7aa + 91161a1 commit 772718d
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 109 deletions.
2 changes: 1 addition & 1 deletion cmd/ynabber/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func main() {
for _, reader := range cfg.Readers {
switch reader {
case "nordigen":
ynabber.Readers = append(ynabber.Readers, nordigen.Reader{Config: &cfg})
ynabber.Readers = append(ynabber.Readers, nordigen.NewReader(&cfg))
default:
log.Fatalf("Unknown reader: %s", reader)
}
Expand Down
11 changes: 5 additions & 6 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ type Config struct {
// PayeeStrip is depreciated please use Nordigen.PayeeStrip instead
PayeeStrip []string `envconfig:"YNABBER_PAYEE_STRIP"`

// SecretID is used to create requisition
NotificationScript string `envconfig:"NOTIFICATION_SCRIPT"`

// Reader and/or writer specific settings
Nordigen Nordigen
YNAB YNAB
Expand All @@ -74,9 +71,6 @@ type Nordigen struct {
// SecretKey is used to create requisition
SecretKey string `envconfig:"NORDIGEN_SECRET_KEY"`

// Use named datafile(relative path in datadir, absolute if starts with slash) instead of default (ynabber-NORDIGEN_BANKID.json)
Datafile string `envconfig:"NORDIGEN_DATAFILE"`

// PayeeSource is a list of sources for Payee candidates, the first method
// that yields a result will be used. Valid options are: unstructured, name
// and additional.
Expand All @@ -98,6 +92,11 @@ type Nordigen struct {
//
// Valid options are: TransactionId, InternalTransactionId
TransactionID string `envconfig:"NORDIGEN_TRANSACTION_ID" default:"TransactionId"`

// RequisitionHook is a exec hook thats executed at various stages of the
// requisition process. The hook is executed with the following arguments:
// <status> <link>
RequisitionHook string `envconfig:"NORDIGEN_REQUISITION_HOOK"`
}

// YNAB related settings
Expand Down
93 changes: 39 additions & 54 deletions reader/nordigen/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,23 @@ import (
"strconv"
"time"

"github.com/martinohansen/ynabber"

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

type Authorization struct {
BankID string
Client nordigen.Client
File string
}
const RequisitionRedirect = "https://raw.githubusercontent.com/martinohansen/ynabber/main/ok.html"

// Store returns a clean path to the requisition file
func (auth Authorization) Store() string {
return path.Clean(auth.File)
// requisitionStore returns a clean path to the requisition file
func (r Reader) requisitionStore() string {
return path.Clean(fmt.Sprintf("%s/%s", r.Config.DataDir, r.Config.Nordigen.BankID))
}

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

switch requisition.Status {
case "EX":
// Create a new requisition if expired
log.Printf("Requisition is expired")
return auth.CreateAndSave(*cfg)
return r.createRequisition()
case "LN":
// Return requisition if it's still valid
return requisition, nil
default:
// Handle unknown status by recreating requisition
log.Printf("Unsupported requisition status: %s", requisition.Status)
return auth.CreateAndSave(*cfg)
}
}

func (auth Authorization) CreateAndSave(cfg ynabber.Config) (nordigen.Requisition, error) {
log.Print("Creating new requisition...")
requisition, err := auth.Create(cfg)
if err != nil {
return nordigen.Requisition{}, fmt.Errorf("AuthorizationCreate: %w", err)
}
err = auth.Save(requisition)
if err != nil {
log.Printf("Failed to write requisition to disk: %s", err)
return r.createRequisition()
}
log.Printf("Requisition stored for reuse: %s", auth.Store())
return requisition, nil
}

func (auth Authorization) Save(requisition nordigen.Requisition) error {
func (r Reader) saveRequisition(requisition nordigen.Requisition) error {
requisitionFile, err := json.Marshal(requisition)
if err != nil {
return err
}

err = os.WriteFile(auth.Store(), requisitionFile, 0644)
err = os.WriteFile(r.requisitionStore(), requisitionFile, 0644)
if err != nil {
return err
}
return nil
}

func (auth Authorization) Create(cfg ynabber.Config) (nordigen.Requisition, error) {
requisition := nordigen.Requisition{
Redirect: "https://raw.githubusercontent.com/martinohansen/ynabber/main/ok.html",
func (r Reader) createRequisition() (nordigen.Requisition, error) {
requisition, err := r.Client.CreateRequisition(nordigen.Requisition{
Redirect: RequisitionRedirect,
Reference: strconv.Itoa(int(time.Now().Unix())),
Agreement: "",
InstitutionId: auth.BankID,
}

r, err := auth.Client.CreateRequisition(requisition)
InstitutionId: r.Config.Nordigen.BankID,
})
if err != nil {
return nordigen.Requisition{}, fmt.Errorf("CreateRequisition: %w", err)
}

auth.Notify(cfg, r)
log.Printf("Initiate requisition by going to: %s", r.Link)
r.requisitionHook(requisition)
log.Printf("Initiate requisition by going to: %s", requisition.Link)

// Keep waiting for the user to accept the requisition
for r.Status != "LN" {
r, err = auth.Client.GetRequisition(r.Id)

for requisition.Status != "LN" {
requisition, err = r.Client.GetRequisition(requisition.Id)
if err != nil {
return nordigen.Requisition{}, fmt.Errorf("GetRequisition: %w", err)
}
time.Sleep(1 * time.Second)
time.Sleep(2 * time.Second)
}

return r, nil
// Store requisition on disk
err = r.saveRequisition(requisition)
if err != nil {
log.Printf("Failed to write requisition to disk: %s", err)
}

return requisition, nil
}

func (auth Authorization) Notify(cfg ynabber.Config, r nordigen.Requisition) {
if cfg.NotificationScript != "" {
cmd := exec.Command(cfg.NotificationScript, r.Link)
// requisitionHook executes the hook with the status and link as arguments
func (r Reader) requisitionHook(req nordigen.Requisition) {
if r.Config.Nordigen.RequisitionHook != "" {
cmd := exec.Command(r.Config.Nordigen.RequisitionHook, req.Status, req.Link)
_, err := cmd.Output()
if err != nil {
log.Println("Could not notify user about new requisition: ", err)
log.Printf("failed to run requisition hook: %s", err)
}
} else {
log.Println("No Notification Script set")
}
}
15 changes: 12 additions & 3 deletions reader/nordigen/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@ package nordigen

import (
"testing"

"github.com/martinohansen/ynabber"
)

func TestStore(t *testing.T) {
auth := Authorization{File: "./ynabber.json"}
want := "ynabber.json"
got := auth.Store()
r := Reader{
Config: &ynabber.Config{
Nordigen: ynabber.Nordigen{
BankID: "foo",
},
DataDir: ".",
},
}
want := "foo"
got := r.requisitionStore()
if want != got {
t.Fatalf("default: %s != %s", want, got)
}
Expand Down
12 changes: 12 additions & 0 deletions reader/nordigen/hooks/example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#! /bin/bash

echo "Hi from hook 👋
status: $1
link: $2
at: $(date)" | tee /tmp/nordigen.log

# If you want to only act on certain events, you key off the first argument like
# this:
if [ "$1" == "CR" ]; then
echo "Requsition created!" | tee -a /tmp/nordigen.log
fi
64 changes: 19 additions & 45 deletions reader/nordigen/nordigen.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package nordigen

import (
"errors"
"fmt"
"log"
"os"
"path"
"regexp"
"strings"

Expand All @@ -15,6 +12,21 @@ import (

type Reader struct {
Config *ynabber.Config

Client *nordigen.Client
}

// NewReader returns a new nordigen reader or panics
func NewReader(cfg *ynabber.Config) Reader {
client, err := nordigen.NewClient(cfg.Nordigen.SecretID, cfg.Nordigen.SecretKey)
if err != nil {
panic("Failed to create nordigen client")
}

return Reader{
Config: cfg,
Client: client,
}
}

// payeeStripNonAlphanumeric removes all non-alphanumeric characters from payee
Expand Down Expand Up @@ -57,53 +69,15 @@ func transactionsToYnabber(cfg ynabber.Config, account ynabber.Account, t nordig
return x, nil
}

// dataFile returns a persistent path
func dataFile(cfg ynabber.Config) string {
dataFile := ""
if cfg.Nordigen.Datafile != "" {
if path.IsAbs(cfg.Nordigen.Datafile) {
dataFile = cfg.Nordigen.Datafile
} else {
dataFile = fmt.Sprintf("%s/%s", cfg.DataDir, cfg.Nordigen.Datafile)
}
} else {
dataFileBankSpecific := fmt.Sprintf("%s/%s-%s.json", cfg.DataDir, "ynabber", cfg.Nordigen.BankID)
dataFileGeneric := fmt.Sprintf("%s/%s.json", cfg.DataDir, "ynabber")
dataFile = dataFileBankSpecific
_, err := os.Stat(dataFileBankSpecific)
if errors.Is(err, os.ErrNotExist) {
_, err := os.Stat(dataFileGeneric)
if errors.Is(err, os.ErrNotExist) {
// If bank specific does not exists and neither does generic, use dataFileBankSpecific
dataFile = dataFileBankSpecific
} else {
// Generic dataFile exists(old naming) but not the bank specific, use dataFileGeneric
dataFile = dataFileGeneric
}
}
}
return dataFile
}

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: r.Config.Nordigen.BankID,
File: dataFile(*r.Config),
}
req, err := Authorization.Wrapper(r.Config)
req, err := r.Requisition()
if err != nil {
return nil, fmt.Errorf("failed to authorize: %w", err)
}

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

account := ynabber.Account{
Expand All @@ -128,7 +102,7 @@ func (r Reader) Bulk() (t []ynabber.Transaction, err error) {

log.Printf("Reading transactions from account: %s", account.Name)

transactions, err := c.GetAccountTransactions(string(account.ID))
transactions, err := r.Client.GetAccountTransactions(string(account.ID))
if err != nil {
return nil, fmt.Errorf("failed to get transactions: %w", err)
}
Expand Down

0 comments on commit 772718d

Please sign in to comment.