feat: implement dashboard service and controller with tests (TDD)
This commit is contained in:
parent
1a13ee3453
commit
1062739832
161
src/AccountTracking.Api.Tests/DashboardServiceTests.cs
Normal file
161
src/AccountTracking.Api.Tests/DashboardServiceTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
30
src/AccountTracking.Api/Controllers/DashboardController.cs
Normal file
30
src/AccountTracking.Api/Controllers/DashboardController.cs
Normal 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));
|
||||
}
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user