Compare commits
2 commits
7f284b1032
...
4cc0888627
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cc0888627 | |||
| 5d6d49ed8b |
4 changed files with 98 additions and 16 deletions
|
|
@ -43,5 +43,20 @@ namespace Schaad.Accounting.Datasets
|
||||||
|
|
||||||
return GetValue(withFxRate);
|
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 maxMonth = transactions.Max(t => t.ValueDate.Month);
|
||||||
|
|
||||||
var list = new List<DataSerie>();
|
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
|
var groupedByMonth = grp
|
||||||
.GroupBy(t => t.ValueDate.Month)
|
.GroupBy(t => t.ValueDate.Month)
|
||||||
|
|
@ -132,7 +132,7 @@ namespace Schaad.Accounting.Services
|
||||||
|
|
||||||
return transactions
|
return transactions
|
||||||
.Where(t => t.TargetAccountId == accountId)
|
.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))))
|
.Select(g => new CategoryTotal(g.Key, g.Sum(t => t.GetValue(account.IsFxAccount, accountId))))
|
||||||
.Where(c => c.Value != 0)
|
.Where(c => c.Value != 0)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,24 @@ public class ChartServiceTestShould
|
||||||
coop.Y[1].ShouldBe(-20m);
|
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]
|
[Fact]
|
||||||
public async Task ShowPositiveValueWhenAccountIsTargetOfBookingTextTransaction()
|
public async Task ShowPositiveValueWhenAccountIsTargetOfBookingTextTransaction()
|
||||||
{
|
{
|
||||||
|
|
@ -230,36 +248,54 @@ public class ChartServiceTestShould
|
||||||
{
|
{
|
||||||
var checking = MakeAccount("chk", 1010);
|
var checking = MakeAccount("chk", 1010);
|
||||||
var groceries = MakeAccount("groc", 4200);
|
var groceries = MakeAccount("groc", 4200);
|
||||||
viewService.GetAccountViewAsync("chk").Returns(AsDataset(checking));
|
viewService.GetAccountViewAsync("groc").Returns(AsDataset(groceries));
|
||||||
viewService.GetTransactionViewListAsync("chk").Returns(new List<TransactionDataset>
|
viewService.GetTransactionViewListAsync("groc").Returns(new List<TransactionDataset>
|
||||||
{
|
{
|
||||||
MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 5), text: "Migros"),
|
MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 5), text: "Migros"),
|
||||||
MakeTransaction(checking, groceries, value: 30m, valueDate: new DateTime(2026, 2, 3), text: "Migros"),
|
MakeTransaction(checking, groceries, value: 30m, valueDate: new DateTime(2026, 2, 3), text: "Migros"),
|
||||||
MakeTransaction(checking, groceries, value: 20m, valueDate: new DateTime(2026, 2, 4), text: "Coop")
|
MakeTransaction(checking, groceries, value: 20m, valueDate: new DateTime(2026, 2, 4), text: "Coop")
|
||||||
});
|
});
|
||||||
|
|
||||||
var totals = await BuildService().GetBookingTextTotalsAsync("chk");
|
var totals = await BuildService().GetBookingTextTotalsAsync("groc");
|
||||||
|
|
||||||
totals.Count.ShouldBe(2);
|
totals.Count.ShouldBe(2);
|
||||||
totals.ShouldContain(t => t.Name == "Migros" && t.Value == -80m);
|
totals.ShouldContain(t => t.Name == "Migros" && t.Value == 80m);
|
||||||
totals.ShouldContain(t => t.Name == "Coop" && t.Value == -20m);
|
totals.ShouldContain(t => t.Name == "Coop" && t.Value == 20m);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task NetOutgoingAndIncomingBookingsWithTheSameTextWhenBuildingTotalsForAccount()
|
public async Task GroupBookingTextsByFirstWordWhenBuildingTotalsForAccount()
|
||||||
{
|
{
|
||||||
var checking = MakeAccount("chk", 1010);
|
var checking = MakeAccount("chk", 1010);
|
||||||
var groceries = MakeAccount("groc", 4200);
|
var groceries = MakeAccount("groc", 4200);
|
||||||
viewService.GetAccountViewAsync("chk").Returns(AsDataset(checking));
|
viewService.GetAccountViewAsync("groc").Returns(AsDataset(groceries));
|
||||||
viewService.GetTransactionViewListAsync("chk").Returns(new List<TransactionDataset>
|
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 ExcludeBookingsWhereAccountIsOriginWhenBuildingTotalsForAccount()
|
||||||
|
{
|
||||||
|
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: "Migros"),
|
MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 5), text: "Migros"),
|
||||||
MakeTransaction(groceries, checking, value: 20m, valueDate: new DateTime(2026, 1, 6), text: "Migros")
|
MakeTransaction(groceries, checking, value: 20m, valueDate: new DateTime(2026, 1, 6), text: "Migros")
|
||||||
});
|
});
|
||||||
|
|
||||||
var totals = await BuildService().GetBookingTextTotalsAsync("chk");
|
var totals = await BuildService().GetBookingTextTotalsAsync("groc");
|
||||||
|
|
||||||
totals.Single().Value.ShouldBe(-30m);
|
totals.Single().Value.ShouldBe(50m);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -267,14 +303,14 @@ public class ChartServiceTestShould
|
||||||
{
|
{
|
||||||
var checking = MakeAccount("chk", 1010);
|
var checking = MakeAccount("chk", 1010);
|
||||||
var groceries = MakeAccount("groc", 4200);
|
var groceries = MakeAccount("groc", 4200);
|
||||||
viewService.GetAccountViewAsync("chk").Returns(AsDataset(checking));
|
viewService.GetAccountViewAsync("groc").Returns(AsDataset(groceries));
|
||||||
viewService.GetTransactionViewListAsync("chk").Returns(new List<TransactionDataset>
|
viewService.GetTransactionViewListAsync("groc").Returns(new List<TransactionDataset>
|
||||||
{
|
{
|
||||||
MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 5), text: "Migros"),
|
MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 5), text: "Migros"),
|
||||||
MakeTransaction(checking, groceries, value: -50m, valueDate: new DateTime(2026, 1, 6), text: "Migros")
|
MakeTransaction(checking, groceries, value: -50m, valueDate: new DateTime(2026, 1, 6), text: "Migros")
|
||||||
});
|
});
|
||||||
|
|
||||||
var totals = await BuildService().GetBookingTextTotalsAsync("chk");
|
var totals = await BuildService().GetBookingTextTotalsAsync("groc");
|
||||||
|
|
||||||
totals.ShouldBeEmpty();
|
totals.ShouldBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ public class TransactionDatasetTestShould
|
||||||
{
|
{
|
||||||
private static Account MakeAccount(string id) => new() { Id = id, Name = id, Currency = "CHF" };
|
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(
|
||||||
new Transaction
|
new Transaction
|
||||||
{
|
{
|
||||||
|
|
@ -17,6 +17,7 @@ public class TransactionDatasetTestShould
|
||||||
TargetAccountId = target.Id,
|
TargetAccountId = target.Id,
|
||||||
Value = value,
|
Value = value,
|
||||||
FxRate = fxRate,
|
FxRate = fxRate,
|
||||||
|
Text = text,
|
||||||
ValueDate = DateTime.Now,
|
ValueDate = DateTime.Now,
|
||||||
BookingDate = DateTime.Now
|
BookingDate = DateTime.Now
|
||||||
},
|
},
|
||||||
|
|
@ -51,4 +52,34 @@ public class TransactionDatasetTestShould
|
||||||
|
|
||||||
transaction.GetValue(withFxRate: true, accountId: "chk").ShouldBe(-200m);
|
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