package report import ( "testing" "git.petrovv.com/nikola/money/internal/model" ) func leg(id int64, amount int64, transferID *int64) model.Transaction { return model.Transaction{ ID: id, AccountSlug: "nlb", Currency: "EUR", MinorDigits: 2, Date: "2026-03-06", AmountMinor: amount, RuleTag: "moving", TransferID: transferID, } } // A matched transfer is money moved, not money spent, so neither leg reaches // the report at all. func TestByTagLeavesTransferLegsOut(t *testing.T) { id := int64(1) rows := ByTag([]model.Transaction{ leg(1, -50000, &id), leg(2, 50000, &id), {Currency: "EUR", MinorDigits: 2, AmountMinor: -2000, RuleTag: "groceries"}, }) if len(rows) != 1 || rows[0].Tag != "groceries" { t.Fatalf("rows = %+v, want only the groceries row", rows) } if rows[0].Out != 2000 { t.Errorf("out = %d, want only the shopping", rows[0].Out) } } // An unpaired leg is not a transfer, so it still counts. Money that left an // account and cannot be shown to have arrived must not quietly vanish from the // report. func TestByTagKeepsUnpairedLegs(t *testing.T) { rows := ByTag([]model.Transaction{leg(1, -50000, nil)}) if len(rows) != 1 || rows[0].Out != 50000 { t.Fatalf("rows = %+v, want the unpaired leg counted", rows) } } // A report scoped to one account or month usually holds one side of a // movement, so the summary counts legs rather than claiming whole transfers. func TestExcludedCountsLegsInView(t *testing.T) { id := int64(1) got := Excluded([]model.Transaction{ leg(1, -50000, &id), {Currency: "EUR", MinorDigits: 2, AmountMinor: -2000, RuleTag: "groceries"}, }) if len(got) != 1 { t.Fatalf("excluded = %+v, want one currency", got) } if got[0].Legs != 1 || got[0].Out != 50000 || got[0].In != 0 || got[0].Currency != "EUR" { t.Errorf("excluded = %+v, want 1 leg and 50000 out in EUR", got[0]) } } // An exchange has its legs in two currencies, so each side reports only what it // saw. Summing the two would be meaningless: there are no rates here. func TestExcludedSplitsAnExchangeByCurrency(t *testing.T) { id := int64(1) arriving := leg(2, 97790, &id) arriving.Currency = "BGN" got := Excluded([]model.Transaction{leg(1, -50000, &id), arriving}) if len(got) != 2 { t.Fatalf("excluded = %+v, want a row per currency", got) } bgn, eur := got[0], got[1] if bgn.Currency != "BGN" || bgn.In != 97790 || bgn.Out != 0 { t.Errorf("BGN row = %+v, want only the arriving leg", bgn) } if eur.Currency != "EUR" || eur.Out != 50000 || eur.In != 0 { t.Errorf("EUR row = %+v, want only the leaving leg", eur) } } // A definition with a tolerance pairs legs that disagree, and the pair leaves // the report with the difference inside it. That difference is real money, so // the summary has to name it or it is simply lost. func TestExcludedReportsTheFee(t *testing.T) { id := int64(1) got := Excluded([]model.Transaction{leg(1, -50000, &id), leg(2, 49500, &id)}) if len(got) != 1 { t.Fatalf("excluded = %+v, want one currency", got) } if got[0].Fee != 500 || got[0].Pairs != 1 || got[0].Legs != 2 { t.Errorf("excluded = %+v, want a 5.00 fee over 1 pair and 2 legs", got[0]) } } // Half a pair cannot say what the other half received, so a report scoped to // one account or month reports the leg it saw and claims no fee at all. func TestExcludedClaimsNoFeeFromHalfAPair(t *testing.T) { id := int64(1) got := Excluded([]model.Transaction{leg(1, -50000, &id)}) if len(got) != 1 { t.Fatalf("excluded = %+v, want one currency", got) } if got[0].Fee != 0 || got[0].Pairs != 0 || got[0].Legs != 1 { t.Errorf("excluded = %+v, want no fee and no complete pair", got[0]) } } // Without a tolerance the two legs are exact opposites, so there is nothing to // report and the line stays as it was. func TestExcludedReportsNoFeeForAnExactPair(t *testing.T) { id := int64(1) got := Excluded([]model.Transaction{leg(1, -50000, &id), leg(2, 50000, &id)}) if len(got) != 1 || got[0].Fee != 0 || got[0].Pairs != 1 { t.Errorf("excluded = %+v, want one pair and no fee", got) } } // The legs of an exchange are in different units, so subtracting one from the // other would produce a number meaning nothing. func TestExcludedClaimsNoFeeAcrossCurrencies(t *testing.T) { id := int64(1) arriving := leg(2, 97790, &id) arriving.Currency = "BGN" for _, row := range Excluded([]model.Transaction{leg(1, -50000, &id), arriving}) { if row.Fee != 0 || row.Pairs != 0 { t.Errorf("%s row = %+v, want no fee across currencies", row.Currency, row) } } }