diff --git a/API/ROLAC.API.Tests/Services/MealAttendanceServiceTests.cs b/API/ROLAC.API.Tests/Services/MealAttendanceServiceTests.cs new file mode 100644 index 0000000..69ce275 --- /dev/null +++ b/API/ROLAC.API.Tests/Services/MealAttendanceServiceTests.cs @@ -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(); + mock.Setup(x => x.HttpContext).Returns(ctx); + return new AppDbContext(new DbContextOptionsBuilder() + .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 + } +} diff --git a/API/ROLAC.API/Services/IMealAttendanceService.cs b/API/ROLAC.API/Services/IMealAttendanceService.cs index 05c122f..ba62136 100644 --- a/API/ROLAC.API/Services/IMealAttendanceService.cs +++ b/API/ROLAC.API/Services/IMealAttendanceService.cs @@ -22,6 +22,13 @@ public interface IMealAttendanceService /// Task SetAsync(DateOnly date, string category, int value); + /// + /// Overwrites all three age-group columns for 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. + /// + Task SetCountsAsync(DateOnly date, int adult, int youth, int kid); + /// Returns the daily counts within the inclusive date range, ordered by date (for the dashboard). Task> GetRangeAsync(DateOnly from, DateOnly to); } diff --git a/API/ROLAC.API/Services/MealAttendanceService.cs b/API/ROLAC.API/Services/MealAttendanceService.cs index 3f82717..f986b31 100644 --- a/API/ROLAC.API/Services/MealAttendanceService.cs +++ b/API/ROLAC.API/Services/MealAttendanceService.cs @@ -82,6 +82,24 @@ public class MealAttendanceService : IMealAttendanceService return await ReadAsync(date); } + public async Task 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> GetRangeAsync(DateOnly from, DateOnly to) { var rows = await _db.MealAttendances.AsNoTracking()