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().
This commit is contained in:
parent
7f284b1032
commit
5d6d49ed8b
4 changed files with 85 additions and 3 deletions
|
|
@ -43,5 +43,20 @@ namespace Schaad.Accounting.Datasets
|
|||
|
||||
return GetValue(withFxRate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// First word of <see cref="Text"/>, used to group related bookings (e.g. "Narbonne Car"
|
||||
/// and "Narbonne Food" both group under "Narbonne").
|
||||
/// </summary>
|
||||
public string GetTextGroup()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Text))
|
||||
{
|
||||
return Text;
|
||||
}
|
||||
|
||||
var spaceIndex = Text.IndexOf(' ');
|
||||
return spaceIndex < 0 ? Text : Text[..spaceIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ namespace Schaad.Accounting.Services
|
|||
var maxMonth = transactions.Max(t => t.ValueDate.Month);
|
||||
|
||||
var list = new List<DataSerie>();
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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<TransactionDataset>
|
||||
{
|
||||
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<TransactionDataset>
|
||||
{
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue