From 5d6d49ed8bf8c024e8bb3ea98c44509f152019d0 Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Fri, 7 Aug 2026 18:23:22 +0200 Subject: [PATCH] Group booking-text charts by first word instead of full text Bookings like "Narbonne Car" and "Narbonne Food" should roll up into one series/slice instead of staying separate, so grouping now uses the text up to the first space via TransactionDataset.GetTextGroup(). --- .../Datasets/TransactionDataset.cs | 15 ++++++++ Schaad.Accounting.Services/ChartService.cs | 4 +-- .../ChartServiceTestShould.cs | 36 +++++++++++++++++++ .../TransactionDatasetTestShould.cs | 33 ++++++++++++++++- 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/Schaad.Accounting.Common/Datasets/TransactionDataset.cs b/Schaad.Accounting.Common/Datasets/TransactionDataset.cs index 9e0f7fd..ab6bb67 100644 --- a/Schaad.Accounting.Common/Datasets/TransactionDataset.cs +++ b/Schaad.Accounting.Common/Datasets/TransactionDataset.cs @@ -43,5 +43,20 @@ namespace Schaad.Accounting.Datasets return GetValue(withFxRate); } + + /// + /// First word of , used to group related bookings (e.g. "Narbonne Car" + /// and "Narbonne Food" both group under "Narbonne"). + /// + public string GetTextGroup() + { + if (string.IsNullOrEmpty(Text)) + { + return Text; + } + + var spaceIndex = Text.IndexOf(' '); + return spaceIndex < 0 ? Text : Text[..spaceIndex]; + } } } \ No newline at end of file diff --git a/Schaad.Accounting.Services/ChartService.cs b/Schaad.Accounting.Services/ChartService.cs index da00b59..2b6aa6b 100644 --- a/Schaad.Accounting.Services/ChartService.cs +++ b/Schaad.Accounting.Services/ChartService.cs @@ -108,7 +108,7 @@ namespace Schaad.Accounting.Services var maxMonth = transactions.Max(t => t.ValueDate.Month); var list = new List(); - foreach (var grp in transactions.GroupBy(t => t.Text)) + foreach (var grp in transactions.GroupBy(t => t.GetTextGroup())) { var groupedByMonth = grp .GroupBy(t => t.ValueDate.Month) @@ -132,7 +132,7 @@ namespace Schaad.Accounting.Services return transactions .Where(t => t.TargetAccountId == accountId) - .GroupBy(t => t.Text) + .GroupBy(t => t.GetTextGroup()) .Select(g => new CategoryTotal(g.Key, g.Sum(t => t.GetValue(account.IsFxAccount, accountId)))) .Where(c => c.Value != 0) .ToList(); diff --git a/Schaad.Accounting.Tests/ChartServiceTestShould.cs b/Schaad.Accounting.Tests/ChartServiceTestShould.cs index 1e6dcb9..eef998f 100644 --- a/Schaad.Accounting.Tests/ChartServiceTestShould.cs +++ b/Schaad.Accounting.Tests/ChartServiceTestShould.cs @@ -193,6 +193,24 @@ public class ChartServiceTestShould coop.Y[1].ShouldBe(-20m); } + [Fact] + public async Task GroupBookingTextsByFirstWordWhenBuildingMonthlySeriesForAccount() + { + var checking = MakeAccount("chk", 1010); + var groceries = MakeAccount("groc", 4200); + viewService.GetAccountViewAsync("chk").Returns(AsDataset(checking)); + viewService.GetTransactionViewListAsync("chk").Returns(new List + { + MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 5), text: "Narbonne Car"), + MakeTransaction(checking, groceries, value: 20m, valueDate: new DateTime(2026, 1, 6), text: "Narbonne Food") + }); + + var series = await BuildService().GetBookingTextExpensesPerMonthAsync("chk"); + + var narbonne = series.Single(s => s.Name == "Narbonne"); + narbonne.Y[0].ShouldBe(-70m); + } + [Fact] public async Task ShowPositiveValueWhenAccountIsTargetOfBookingTextTransaction() { @@ -245,6 +263,24 @@ public class ChartServiceTestShould totals.ShouldContain(t => t.Name == "Coop" && t.Value == -20m); } + [Fact] + public async Task GroupBookingTextsByFirstWordWhenBuildingTotalsForAccount() + { + var checking = MakeAccount("chk", 1010); + var groceries = MakeAccount("groc", 4200); + viewService.GetAccountViewAsync("groc").Returns(AsDataset(groceries)); + viewService.GetTransactionViewListAsync("groc").Returns(new List + { + MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 5), text: "Narbonne Car"), + MakeTransaction(checking, groceries, value: 20m, valueDate: new DateTime(2026, 1, 6), text: "Narbonne Food") + }); + + var totals = await BuildService().GetBookingTextTotalsAsync("groc"); + + totals.Single().Name.ShouldBe("Narbonne"); + totals.Single().Value.ShouldBe(70m); + } + [Fact] public async Task NetOutgoingAndIncomingBookingsWithTheSameTextWhenBuildingTotalsForAccount() { diff --git a/Schaad.Accounting.Tests/TransactionDatasetTestShould.cs b/Schaad.Accounting.Tests/TransactionDatasetTestShould.cs index db931cd..50053b6 100644 --- a/Schaad.Accounting.Tests/TransactionDatasetTestShould.cs +++ b/Schaad.Accounting.Tests/TransactionDatasetTestShould.cs @@ -8,7 +8,7 @@ public class TransactionDatasetTestShould { private static Account MakeAccount(string id) => new() { Id = id, Name = id, Currency = "CHF" }; - private static TransactionDataset MakeTransaction(Account origin, Account target, decimal value, decimal? fxRate = null) => + private static TransactionDataset MakeTransaction(Account origin, Account target, decimal value, decimal? fxRate = null, string text = "") => new( new Transaction { @@ -17,6 +17,7 @@ public class TransactionDatasetTestShould TargetAccountId = target.Id, Value = value, FxRate = fxRate, + Text = text, ValueDate = DateTime.Now, BookingDate = DateTime.Now }, @@ -51,4 +52,34 @@ public class TransactionDatasetTestShould transaction.GetValue(withFxRate: true, accountId: "chk").ShouldBe(-200m); } + + [Fact] + public void ReturnFirstWordWhenTextHasMultipleWords() + { + var checking = MakeAccount("chk"); + var groceries = MakeAccount("groc"); + var transaction = MakeTransaction(checking, groceries, value: 50m, text: "Narbonne Car"); + + transaction.GetTextGroup().ShouldBe("Narbonne"); + } + + [Fact] + public void ReturnWholeTextWhenTextHasNoSpace() + { + var checking = MakeAccount("chk"); + var groceries = MakeAccount("groc"); + var transaction = MakeTransaction(checking, groceries, value: 50m, text: "Migros"); + + transaction.GetTextGroup().ShouldBe("Migros"); + } + + [Fact] + public void ReturnEmptyStringWhenTextIsEmpty() + { + var checking = MakeAccount("chk"); + var groceries = MakeAccount("groc"); + var transaction = MakeTransaction(checking, groceries, value: 50m, text: ""); + + transaction.GetTextGroup().ShouldBe(""); + } }