using Microsoft.EntityFrameworkCore; using ROLAC.API.Data; using ROLAC.API.DTOs.MealAttendance; using ROLAC.API.Entities; namespace ROLAC.API.Services; public class MealAttendanceService : IMealAttendanceService { private readonly AppDbContext _db; public MealAttendanceService(AppDbContext db) => _db = db; // Server local time is assumed to match the church's local day. public DateOnly ServiceDay { get { var today = DateOnly.FromDateTime(DateTime.Now); return today.AddDays(-(int)today.DayOfWeek); } } public async Task GetOrCreateAsync(DateOnly date) { await EnsureRowAsync(date); return await ReadAsync(date); } public async Task IncrementAsync(DateOnly date, string category, int delta) { await EnsureRowAsync(date); var rows = _db.MealAttendances.Where(a => a.AttendanceDate == date); // Atomic, race-free server-side increment; never let a column go below zero. switch (category.Trim().ToLowerInvariant()) { case "adult": await rows.ExecuteUpdateAsync(s => s.SetProperty( a => a.AdultCount, a => a.AdultCount + delta < 0 ? 0 : a.AdultCount + delta)); break; case "youth": await rows.ExecuteUpdateAsync(s => s.SetProperty( a => a.YouthCount, a => a.YouthCount + delta < 0 ? 0 : a.YouthCount + delta)); break; case "kid": await rows.ExecuteUpdateAsync(s => s.SetProperty( a => a.KidCount, a => a.KidCount + delta < 0 ? 0 : a.KidCount + delta)); break; default: throw new ArgumentException($"Unknown attendance category '{category}'.", nameof(category)); } return await ReadAsync(date); } public async Task SetAsync(DateOnly date, string category, int value) { await EnsureRowAsync(date); // Counts can never be negative; clamp before writing. var clamped = value < 0 ? 0 : value; var rows = _db.MealAttendances.Where(a => a.AttendanceDate == date); switch (category.Trim().ToLowerInvariant()) { case "adult": await rows.ExecuteUpdateAsync(s => s.SetProperty(a => a.AdultCount, clamped)); break; case "youth": await rows.ExecuteUpdateAsync(s => s.SetProperty(a => a.YouthCount, clamped)); break; case "kid": await rows.ExecuteUpdateAsync(s => s.SetProperty(a => a.KidCount, clamped)); break; default: throw new ArgumentException($"Unknown attendance category '{category}'.", nameof(category)); } 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() .Where(a => a.AttendanceDate >= from && a.AttendanceDate <= to) .OrderBy(a => a.AttendanceDate) .ToListAsync(); return rows.Select(ToDto).ToList(); } // ── helpers ────────────────────────────────────────────────────────────── // Insert a zeroed row if today's row doesn't exist yet. A concurrent insert // that wins the unique index throws DbUpdateException — harmless, the row exists. private async Task EnsureRowAsync(DateOnly date) { if (await _db.MealAttendances.AsNoTracking().AnyAsync(a => a.AttendanceDate == date)) return; var row = new MealAttendance { AttendanceDate = date }; _db.MealAttendances.Add(row); try { await _db.SaveChangesAsync(); } catch (DbUpdateException) { _db.Entry(row).State = EntityState.Detached; } } private async Task ReadAsync(DateOnly date) { var row = await _db.MealAttendances.AsNoTracking() .FirstOrDefaultAsync(a => a.AttendanceDate == date); return row is null ? new AttendanceCountsDto { Date = date.ToString("yyyy-MM-dd") } : ToDto(row); } private static AttendanceCountsDto ToDto(MealAttendance row) => new() { Date = row.AttendanceDate.ToString("yyyy-MM-dd"), Adult = row.AdultCount, Youth = row.YouthCount, Kid = row.KidCount, }; }