feat: implement dashboard service and controller with tests (TDD)

This commit is contained in:
Martin 2026-03-20 00:49:27 +01:00
parent 1a13ee3453
commit 1062739832
3 changed files with 199 additions and 8 deletions

View File

@ -0,0 +1,161 @@
using AccountTracking.Api.Data;
using AccountTracking.Api.Models;
using AccountTracking.Api.Services;
using Microsoft.EntityFrameworkCore;
namespace AccountTracking.Api.Tests;
public class DashboardServiceTests
{
private static AppDbContext CreateDb(params Transaction[] seed)
{
var opts = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
var db = new AppDbContext(opts);
db.Transactions.AddRange(seed);
db.SaveChanges();
return db;
}
private static Transaction Tx(string date, decimal amount, decimal balance,
string category = "Test", string txId = "") =>
new()
{
BookingDate = DateOnly.Parse(date),
Amount = amount,
Balance = balance,
Category = category,
TransactionId = txId == "" ? Guid.NewGuid().ToString() : txId,
Currency = "CZK",
AccountNumber = "test"
};
// ── Summary ──────────────────────────────────────────────────────────────
[Fact]
public async Task Summary_YearOnly_ReturnsTotalsForWholeYear()
{
using var db = CreateDb(
Tx("2025-01-10", -1000, 9000),
Tx("2025-06-15", -2000, 7000),
Tx("2025-03-01", 5000, 12000),
Tx("2024-12-31", -500, 9500) // different year — excluded
);
var svc = new DashboardService(db);
var result = await svc.GetSummaryAsync(2025, null);
Assert.Equal(3000m, result.TotalSpent); // 1000 + 2000
Assert.Equal(5000m, result.TotalIncome);
}
[Fact]
public async Task Summary_YearAndMonth_ReturnsTotalsForMonth()
{
using var db = CreateDb(
Tx("2025-03-10", -1000, 9000),
Tx("2025-03-20", -500, 8500),
Tx("2025-04-01", -800, 7700) // different month — excluded
);
var svc = new DashboardService(db);
var result = await svc.GetSummaryAsync(2025, 3);
Assert.Equal(1500m, result.TotalSpent);
Assert.Equal(0m, result.TotalIncome);
}
// ── SpendingByCategory ───────────────────────────────────────────────────
[Fact]
public async Task SpendingByCategory_ReturnsAbsoluteValuesSortedDescending()
{
using var db = CreateDb(
Tx("2025-03-01", -100, 900, "A"),
Tx("2025-03-02", -300, 600, "B"),
Tx("2025-03-03", -200, 400, "A")
);
var svc = new DashboardService(db);
var result = await svc.GetSpendingByCategoryAsync(2025, 3);
Assert.Equal(2, result.Count);
Assert.Equal("A", result[0].Category);
Assert.Equal(300m, result[0].Total); // 100 + 200
Assert.Equal("B", result[1].Category);
Assert.Equal(300m, result[1].Total);
}
[Fact]
public async Task SpendingByCategory_ExcludesPositiveAmounts()
{
using var db = CreateDb(
Tx("2025-03-01", -500, 9500, "Potraviny"),
Tx("2025-03-02", 200, 9700, "Potraviny") // refund — excluded
);
var svc = new DashboardService(db);
var result = await svc.GetSpendingByCategoryAsync(2025, 3);
Assert.Single(result);
Assert.Equal(500m, result[0].Total);
}
// ── MonthlyBalances ──────────────────────────────────────────────────────
[Fact]
public async Task MonthlyBalances_ReturnsLastBalancePerMonth()
{
using var db = CreateDb(
Tx("2025-01-10", -100, 900),
Tx("2025-01-20", -200, 700), // last in Jan — use this balance
Tx("2025-03-05", -150, 550) // March — Feb omitted (no transactions)
);
var svc = new DashboardService(db);
var result = await svc.GetMonthlyBalancesAsync(2025);
Assert.Equal(2, result.Count);
Assert.Equal(1, result[0].Month);
Assert.Equal(700m, result[0].ClosingBalance);
Assert.Equal(3, result[1].Month);
Assert.Equal(550m, result[1].ClosingBalance);
}
// ── CumulativeSpending ───────────────────────────────────────────────────
[Fact]
public async Task CumulativeSpending_FillsGapsAndAccumulates()
{
using var db = CreateDb(
Tx("2025-03-01", -100, 900),
Tx("2025-03-03", -200, 700), // gap on day 2
Tx("2025-03-03", -50, 650) // two transactions on same day
);
var svc = new DashboardService(db);
var result = await svc.GetCumulativeSpendingAsync(2025, 3);
// Day 1: 100, Day 2: filled = 100, Day 3: 100 + 250 = 350
Assert.Equal(3, result.Count);
Assert.Equal(1, result[0].Day); Assert.Equal(100m, result[0].CumulativeSpent);
Assert.Equal(2, result[1].Day); Assert.Equal(100m, result[1].CumulativeSpent);
Assert.Equal(3, result[2].Day); Assert.Equal(350m, result[2].CumulativeSpent);
}
[Fact]
public async Task CumulativeSpending_ExcludesPositiveAmounts()
{
using var db = CreateDb(
Tx("2025-03-01", -100, 900),
Tx("2025-03-01", 500, 1400) // income — not counted
);
var svc = new DashboardService(db);
var result = await svc.GetCumulativeSpendingAsync(2025, 3);
Assert.Single(result);
Assert.Equal(100m, result[0].CumulativeSpent);
}
}

View File

@ -0,0 +1,30 @@
using AccountTracking.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AccountTracking.Api.Controllers;
[ApiController]
[Route("api/dashboard")]
[Authorize]
public class DashboardController : ControllerBase
{
private readonly DashboardService _svc;
public DashboardController(DashboardService svc) => _svc = svc;
[HttpGet("summary")]
public async Task<IActionResult> Summary([FromQuery] int year, [FromQuery] int? month)
=> Ok(await _svc.GetSummaryAsync(year, month));
[HttpGet("spending-by-category")]
public async Task<IActionResult> SpendingByCategory([FromQuery] int year, [FromQuery] int? month)
=> Ok(await _svc.GetSpendingByCategoryAsync(year, month));
[HttpGet("monthly-balances")]
public async Task<IActionResult> MonthlyBalances([FromQuery] int year)
=> Ok(await _svc.GetMonthlyBalancesAsync(year));
[HttpGet("cumulative-spending")]
public async Task<IActionResult> CumulativeSpending([FromQuery] int year, [FromQuery] int month)
=> Ok(await _svc.GetCumulativeSpendingAsync(year, month));
}

View File

@ -39,20 +39,20 @@ public class DashboardService(AppDbContext db)
public async Task<List<MonthlyBalanceDto>> GetMonthlyBalancesAsync(int year)
{
// For each month: get the balance of the transaction with the latest booking_date
// (highest id as tiebreaker for same-date rows)
// Fetch minimal columns, then group in memory to get closing balance per month
// (latest booking_date, highest id as tiebreaker)
var rows = await db.Transactions
.Where(t => t.BookingDate.Year == year)
.Select(t => new { t.BookingDate, t.Id, t.Balance })
.ToListAsync();
return rows
.GroupBy(t => t.BookingDate.Month)
.Select(g => new MonthlyBalanceDto(
g.Key,
g.OrderByDescending(t => t.BookingDate)
.ThenByDescending(t => t.Id)
.First().Balance))
g.OrderByDescending(t => t.BookingDate).ThenByDescending(t => t.Id).First().Balance))
.OrderBy(r => r.Month)
.ToListAsync();
return rows;
.ToList();
}
public async Task<List<CumulativeSpendingDto>> GetCumulativeSpendingAsync(int year, int month)