package parser import "testing" // A page as pdftotext -layout renders it: a header, transaction lines ending // in amount and balance, and indented continuation lines carrying wrapped // descriptions and the counterparty IBAN. // The description column sits at 15, which is what the script's // CONTINUATION_INDENT tells us about the real layout. const nlbPage = ` NLB d.d. IZPISEK 001/2026 DATUM OPIS ZNESEK STANJE 02.01.26 PLACILO S KARTICO -45,20 1.234,56 LIDL SOFIA 4412 SI56 1234 5678 9012 345 05.01.26 PRILIV PLACE +2.500,00 3.734,56 ACME DOO 10.01.26 PRENOS NA VARCEVALNI -500,00 3.234,56 15.01.26 NEGATIVNO STANJE -3.300,00 65,44- Stran 1 ` func TestParseNLBText(t *testing.T) { txns, err := parseNLBText(nlbPage, 2) if err != nil { t.Fatal(err) } if len(txns) != 4 { t.Fatalf("got %d transactions, want 4: %+v", len(txns), txns) } first := txns[0] if first.Date != "2026-01-02" { t.Errorf("date = %q, want 2026-01-02", first.Date) } // The continuation line is appended to the description. if first.Description != "PLACILO S KARTICO LIDL SOFIA 4412" { t.Errorf("description = %q", first.Description) } // ...and the IBAN in its second column becomes the counterparty. if first.Counterparty != "SI56 1234 5678 9012 345" { t.Errorf("counterparty = %q", first.Counterparty) } if first.AmountMinor != -4520 { t.Errorf("amount = %d, want -4520", first.AmountMinor) } if first.BalanceMinor == nil || *first.BalanceMinor != 123456 { t.Errorf("balance = %v, want 123456", first.BalanceMinor) } if txns[1].AmountMinor != 250000 { t.Errorf("credit amount = %d, want 250000", txns[1].AmountMinor) } if txns[1].Description != "PRILIV PLACE ACME DOO" { t.Errorf("description = %q", txns[1].Description) } // A transaction with no continuation line keeps an empty counterparty. if txns[2].Counterparty != "" { t.Errorf("counterparty = %q, want empty", txns[2].Counterparty) } // A trailing minus marks a negative balance. if txns[3].BalanceMinor == nil || *txns[3].BalanceMinor != -6544 { t.Errorf("balance = %v, want -6544", txns[3].BalanceMinor) } } // A wrapped line at the top of a page must not attach to the last transaction // of the previous page. func TestParseNLBPageBoundary(t *testing.T) { text := "02.01.26 FIRST -10,00 100,00\n" + "\f" + " STRAY CONTINUATION AT TOP OF PAGE\n" + "03.01.26 SECOND -20,00 80,00\n" + " REAL CONTINUATION\n" txns, err := parseNLBText(text, 2) if err != nil { t.Fatal(err) } if len(txns) != 2 { t.Fatalf("got %d transactions, want 2", len(txns)) } if txns[0].Description != "FIRST" { t.Errorf("first description = %q, want FIRST (page furniture must not leak in)", txns[0].Description) } if txns[1].Description != "SECOND REAL CONTINUATION" { t.Errorf("second description = %q", txns[1].Description) } } // Shallowly indented lines are footers, not wrapped descriptions. func TestParseNLBIgnoresShallowIndent(t *testing.T) { text := "02.01.26 FIRST -10,00 100,00\n" + " Stran 1 od 2\n" txns, err := parseNLBText(text, 2) if err != nil { t.Fatal(err) } if len(txns) != 1 || txns[0].Description != "FIRST" { t.Errorf("got %+v, want a single FIRST transaction", txns) } } // A date-led line without an amount and balance is a real problem, not // something to silently drop. func TestParseNLBRejectsMalformedLine(t *testing.T) { text := "02.01.26 NO AMOUNT OR BALANCE HERE\n" if _, err := parseNLBText(text, 2); err == nil { t.Error("expected an error for a transaction line with no amount") } }