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("");
+ }
}