Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(nordigen): nordea duplicate transactions #90

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 27 additions & 47 deletions reader/nordigen/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,14 @@ import (
"github.com/martinohansen/ynabber"
)

type Mapper interface {
Map(ynabber.Account, nordigen.Transaction) (ynabber.Transaction, error)
}

// Mapper returns a mapper to transform the banks transaction to Ynabber
func (r Reader) Mapper() Mapper {
switch r.Config.Nordigen.BankID {
case "NORDEA_NDEADKKK":
return Nordea{}
// Mapper uses the most specific mapper for the bank in question
func (r Reader) Mapper(a ynabber.Account, t nordigen.Transaction) (*ynabber.Transaction, error) {
switch {
case strings.HasPrefix(r.Config.Nordigen.BankID, "NORDEA_"):
return r.nordeaMapper(a, t)

default:
return Default{
PayeeSource: r.Config.Nordigen.PayeeSource,
TransactionID: r.Config.Nordigen.TransactionID,
}
return r.defaultMapper(a, t)
}
}

Expand All @@ -44,27 +37,24 @@ func parseDate(t nordigen.Transaction) (time.Time, error) {
return date, nil
}

// Default mapping for all banks unless a more specific mapping exists
type Default struct {
PayeeSource []string
TransactionID string
}
// defaultMapper is generic and tries to identify the appropriate mapping
func (r Reader) defaultMapper(a ynabber.Account, t nordigen.Transaction) (*ynabber.Transaction, error) {
PayeeSource := r.Config.Nordigen.PayeeSource
TransactionID := r.Config.Nordigen.TransactionID

// Map t using the default mapper
func (mapper Default) Map(a ynabber.Account, t nordigen.Transaction) (ynabber.Transaction, error) {
amount, err := parseAmount(t)
if err != nil {
return ynabber.Transaction{}, err
return nil, err
}
date, err := parseDate(t)
if err != nil {
return ynabber.Transaction{}, err
return nil, err
}

// Get the Payee from the first data source that returns data in the order
// defined by config
payee := ""
for _, source := range mapper.PayeeSource {
for _, source := range PayeeSource {
if payee == "" {
switch source {
case "unstructured":
Expand Down Expand Up @@ -94,23 +84,23 @@ func (mapper Default) Map(a ynabber.Account, t nordigen.Transaction) (ynabber.Tr

default:
// Return an error if source is not recognized
return ynabber.Transaction{}, fmt.Errorf("unrecognized PayeeSource: %s", source)
return nil, fmt.Errorf("unrecognized PayeeSource: %s", source)
}
}
}

// Set the transaction ID according to config
var id string
switch mapper.TransactionID {
switch TransactionID {
case "InternalTransactionId":
id = t.InternalTransactionId
case "TransactionId":
id = t.TransactionId
default:
return ynabber.Transaction{}, fmt.Errorf("unrecognized TransactionID: %s", mapper.TransactionID)
return nil, fmt.Errorf("unrecognized TransactionID: %s", TransactionID)
}

return ynabber.Transaction{
return &ynabber.Transaction{
Account: a,
ID: ynabber.ID(id),
Date: date,
Expand All @@ -120,26 +110,16 @@ func (mapper Default) Map(a ynabber.Account, t nordigen.Transaction) (ynabber.Tr
}, nil
}

// Nordea implements a specific mapper for Nordea
type Nordea struct{}

// Map t using the Nordea mapper
func (mapper Nordea) Map(a ynabber.Account, t nordigen.Transaction) (ynabber.Transaction, error) {
amount, err := parseAmount(t)
if err != nil {
return ynabber.Transaction{}, err
}
date, err := parseDate(t)
if err != nil {
return ynabber.Transaction{}, err
// nordeaMapper handles Nordea transactions specifically
func (r Reader) nordeaMapper(a ynabber.Account, t nordigen.Transaction) (*ynabber.Transaction, error) {
// They now maintain two transactions for every actual transaction. First
// they show up prefixed with a ID prefixed with a H, sometime later another
// transaction describing the same transactions shows up with a new ID
// prefixed with a P instead. The H transaction matches the date which its
// visible in my account so i will discard the P transactions for now.
if strings.HasPrefix(t.TransactionId, "P") {
return nil, nil
}

return ynabber.Transaction{
Account: a,
ID: ynabber.ID(t.InternalTransactionId),
Date: date,
Payee: ynabber.Payee(payeeStripNonAlphanumeric(t.RemittanceInformationUnstructured)),
Memo: t.RemittanceInformationUnstructured,
Amount: ynabber.MilliunitsFromAmount(amount),
}, nil
return r.defaultMapper(a, t)
}
15 changes: 10 additions & 5 deletions reader/nordigen/nordigen.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ func payeeStripNonAlphanumeric(payee string) (x string) {
return strings.TrimSpace(x)
}

func (r Reader) toYnabber(a ynabber.Account, t nordigen.Transaction) (ynabber.Transaction, error) {
transaction, err := r.Mapper().Map(a, t)
func (r Reader) toYnabber(a ynabber.Account, t nordigen.Transaction) (*ynabber.Transaction, error) {
transaction, err := r.Mapper(a, t)
if err != nil {
return ynabber.Transaction{}, err
return nil, err
}

// Execute strip method on payee if defined in config
Expand All @@ -63,8 +63,13 @@ func (r Reader) toYnabbers(a ynabber.Account, t nordigen.AccountTransactions) ([
}

// Append transaction
r.logger.Debug("mapped transaction", "from", v, "to", transaction)
y = append(y, transaction)
if transaction != nil {
r.logger.Debug("mapped transaction", "from", v, "to", transaction)
y = append(y, *transaction)
} else {
r.logger.Debug("skipping", "transaction", v)
}

}
return y, nil
}
Expand Down
17 changes: 9 additions & 8 deletions reader/nordigen/nordigen_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nordigen

import (
"reflect"
"testing"
"time"

Expand All @@ -22,7 +23,7 @@ func TestToYnabber(t *testing.T) {
bankID string
reader Reader
args args
want ynabber.Transaction
want *ynabber.Transaction
wantErr bool
}{
{
Expand All @@ -33,10 +34,10 @@ func TestToYnabber(t *testing.T) {
args: args{
account: ynabber.Account{Name: "foo", IBAN: "bar"},
t: nordigen.Transaction{
InternalTransactionId: "H00000000000000000000",
EntryReference: "",
BookingDate: "2023-02-24",
ValueDate: "2023-02-24",
TransactionId: "H00000000000000000000",
EntryReference: "",
BookingDate: "2023-02-24",
ValueDate: "2023-02-24",
TransactionAmount: struct {
Amount string "json:\"amount,omitempty\""
Currency string "json:\"currency,omitempty\""
Expand All @@ -56,7 +57,7 @@ func TestToYnabber(t *testing.T) {
BankTransactionCode: "",
AdditionalInformation: "VISA KØB"},
},
want: ynabber.Transaction{
want: &ynabber.Transaction{
Account: ynabber.Account{Name: "foo", IBAN: "bar"},
ID: ynabber.ID("H00000000000000000000"),
Date: time.Date(2023, time.February, 24, 0, 0, 0, 0, time.UTC),
Expand Down Expand Up @@ -96,7 +97,7 @@ func TestToYnabber(t *testing.T) {
BankTransactionCode: "PURCHASE",
AdditionalInformation: "PASCAL AS"},
},
want: ynabber.Transaction{
want: &ynabber.Transaction{
Account: ynabber.Account{Name: "foo", IBAN: "bar"},
ID: ynabber.ID("foobar"),
Date: time.Date(2023, time.February, 24, 0, 0, 0, 0, time.UTC),
Expand All @@ -120,7 +121,7 @@ func TestToYnabber(t *testing.T) {
t.Errorf("error = %+v, wantErr %+v", err, tt.wantErr)
return
}
if got != tt.want {
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got = \n%+v, want \n%+v", got, tt.want)
}
})
Expand Down
Loading