From ee1dba09c3ccffac4098d96f9d3f1ab3c0624dc4 Mon Sep 17 00:00:00 2001 From: Martin Hansen Date: Wed, 18 Sep 2024 22:24:30 +0200 Subject: [PATCH] fix(nordigen): nordea duplicate transactions Nordea never sleeps, this is a fix for a duplicate transactions which is caused by Nordea showing the same transaction twice with different transaction IDs. BREAKING CHANGE: Nordea users will have new import IDs generated which causes duplicate transactions. To fix this set the YNAB_FROM_DATE value to the day you start using this version. commit-id:40d40309 --- reader/nordigen/mapper.go | 74 ++++++++++++-------------------- reader/nordigen/nordigen.go | 15 ++++--- reader/nordigen/nordigen_test.go | 17 ++++---- 3 files changed, 46 insertions(+), 60 deletions(-) diff --git a/reader/nordigen/mapper.go b/reader/nordigen/mapper.go index 6013f80..bab05fd 100644 --- a/reader/nordigen/mapper.go +++ b/reader/nordigen/mapper.go @@ -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) } } @@ -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": @@ -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, @@ -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) } diff --git a/reader/nordigen/nordigen.go b/reader/nordigen/nordigen.go index 93d044e..367d800 100644 --- a/reader/nordigen/nordigen.go +++ b/reader/nordigen/nordigen.go @@ -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 @@ -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 } diff --git a/reader/nordigen/nordigen_test.go b/reader/nordigen/nordigen_test.go index 15c6fc1..fd25e6d 100644 --- a/reader/nordigen/nordigen_test.go +++ b/reader/nordigen/nordigen_test.go @@ -1,6 +1,7 @@ package nordigen import ( + "reflect" "testing" "time" @@ -22,7 +23,7 @@ func TestToYnabber(t *testing.T) { bankID string reader Reader args args - want ynabber.Transaction + want *ynabber.Transaction wantErr bool }{ { @@ -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\"" @@ -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), @@ -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), @@ -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) } })