feat(attendance): add SetCountsAsync to set all three age groups for a date
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using ROLAC.API.Data;
|
||||
using ROLAC.API.Data.Interceptors;
|
||||
using ROLAC.API.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace ROLAC.API.Tests.Services;
|
||||
|
||||
public class MealAttendanceServiceTests
|
||||
{
|
||||
// MealAttendance is auditable, so the InMemory provider requires CreatedBy/UpdatedBy
|
||||
// to be set before insert. Wire in the AuditSaveChangesInterceptor (as the other
|
||||
// service tests do) so those columns are stamped automatically on SaveChanges.
|
||||
private static AppDbContext BuildDb()
|
||||
{
|
||||
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "test-user") };
|
||||
var ctx = new DefaultHttpContext { User = new(new ClaimsIdentity(claims)) };
|
||||
var mock = new Mock<IHttpContextAccessor>();
|
||||
mock.Setup(x => x.HttpContext).Returns(ctx);
|
||||
return new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.AddInterceptors(new AuditSaveChangesInterceptor(
|
||||
new ROLAC.API.Services.Logging.CurrentUserAccessor(mock.Object))).Options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetCountsAsync_CreatesRowWhenMissing_AndReturnsTotals()
|
||||
{
|
||||
using var db = BuildDb();
|
||||
var svc = new MealAttendanceService(db);
|
||||
var date = new DateOnly(2026, 5, 31);
|
||||
|
||||
var result = await svc.SetCountsAsync(date, adult: 40, youth: 12, kid: 8);
|
||||
|
||||
Assert.Equal("2026-05-31", result.Date);
|
||||
Assert.Equal(40, result.Adult);
|
||||
Assert.Equal(12, result.Youth);
|
||||
Assert.Equal(8, result.Kid);
|
||||
Assert.Single(db.MealAttendances.Where(a => a.AttendanceDate == date));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetCountsAsync_OverwritesExistingRow_AndClampsNegativesToZero()
|
||||
{
|
||||
using var db = BuildDb();
|
||||
var svc = new MealAttendanceService(db);
|
||||
var date = new DateOnly(2026, 5, 31);
|
||||
await svc.SetCountsAsync(date, 40, 12, 8);
|
||||
|
||||
var result = await svc.SetCountsAsync(date, adult: 50, youth: -3, kid: 0);
|
||||
|
||||
Assert.Equal(50, result.Adult);
|
||||
Assert.Equal(0, result.Youth); // negative clamped to zero
|
||||
Assert.Equal(0, result.Kid);
|
||||
Assert.Single(db.MealAttendances.Where(a => a.AttendanceDate == date)); // still one row
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,13 @@ public interface IMealAttendanceService
|
||||
/// </summary>
|
||||
Task<AttendanceCountsDto> SetAsync(DateOnly date, string category, int value);
|
||||
|
||||
/// <summary>
|
||||
/// Overwrites all three age-group columns for <paramref name="date"/> with absolute
|
||||
/// values (each clamped at zero), creating the row if it does not exist, and returns
|
||||
/// the resulting authoritative counts. Used by the back-office Sunday-attendance editor.
|
||||
/// </summary>
|
||||
Task<AttendanceCountsDto> SetCountsAsync(DateOnly date, int adult, int youth, int kid);
|
||||
|
||||
/// <summary>Returns the daily counts within the inclusive date range, ordered by date (for the dashboard).</summary>
|
||||
Task<IReadOnlyList<AttendanceCountsDto>> GetRangeAsync(DateOnly from, DateOnly to);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,24 @@ public class MealAttendanceService : IMealAttendanceService
|
||||
return await ReadAsync(date);
|
||||
}
|
||||
|
||||
public async Task<AttendanceCountsDto> SetCountsAsync(DateOnly date, int adult, int youth, int kid)
|
||||
{
|
||||
var row = await _db.MealAttendances.FirstOrDefaultAsync(a => a.AttendanceDate == date);
|
||||
if (row is null)
|
||||
{
|
||||
row = new MealAttendance { AttendanceDate = date };
|
||||
_db.MealAttendances.Add(row);
|
||||
}
|
||||
|
||||
// Counts can never be negative; clamp before writing.
|
||||
row.AdultCount = adult < 0 ? 0 : adult;
|
||||
row.YouthCount = youth < 0 ? 0 : youth;
|
||||
row.KidCount = kid < 0 ? 0 : kid;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return ToDto(row);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AttendanceCountsDto>> GetRangeAsync(DateOnly from, DateOnly to)
|
||||
{
|
||||
var rows = await _db.MealAttendances.AsNoTracking()
|
||||
|
||||
Reference in New Issue
Block a user