feat(giving): single-entry giving service with paging + lock guard
Adds GivingListItemDto, GivingDto, CreateGivingRequest, UpdateGivingRequest DTOs; IGivingService interface; GivingService implementation with category/date filtering, OfferingSession lock guard (Submitted/Reconciled), and DI registration in Program.cs. Covered by 4 xUnit tests (TDD: red → green). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Moq;
|
||||
using ROLAC.API.Data;
|
||||
using ROLAC.API.Data.Interceptors;
|
||||
using ROLAC.API.DTOs.Giving;
|
||||
using ROLAC.API.Entities;
|
||||
using ROLAC.API.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace ROLAC.API.Tests.Services;
|
||||
|
||||
public class GivingServiceTests
|
||||
{
|
||||
private static IHttpContextAccessor BuildAccessor(string userId = "test-user")
|
||||
{
|
||||
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId) };
|
||||
var ctx = new DefaultHttpContext { User = new(new ClaimsIdentity(claims)) };
|
||||
var mock = new Mock<IHttpContextAccessor>();
|
||||
mock.Setup(x => x.HttpContext).Returns(ctx);
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
private static AppDbContext BuildDb(string userId = "test-user")
|
||||
{
|
||||
var interceptor = new AuditSaveChangesInterceptor(BuildAccessor(userId));
|
||||
return new AppDbContext(
|
||||
new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.AddInterceptors(interceptor)
|
||||
.Options);
|
||||
}
|
||||
|
||||
private static async Task<int> SeedCategoryAsync(AppDbContext db)
|
||||
{
|
||||
var c = new GivingCategory { Name_en = "Tithe", IsActive = true };
|
||||
db.GivingCategories.Add(c);
|
||||
await db.SaveChangesAsync();
|
||||
return c.Id;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_PersistsGiving()
|
||||
{
|
||||
using var db = BuildDb();
|
||||
var catId = await SeedCategoryAsync(db);
|
||||
var svc = new GivingService(db);
|
||||
|
||||
var id = await svc.CreateAsync(new CreateGivingRequest
|
||||
{
|
||||
GivingCategoryId = catId, Amount = 100m, PaymentMethod = "Cash",
|
||||
GivingDate = new DateOnly(2026, 5, 31), IsAnonymous = true,
|
||||
});
|
||||
|
||||
var saved = await db.Givings.FindAsync(id);
|
||||
Assert.NotNull(saved);
|
||||
Assert.Equal(100m, saved!.Amount);
|
||||
Assert.Null(saved.OfferingSessionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPagedAsync_FiltersByCategory()
|
||||
{
|
||||
using var db = BuildDb();
|
||||
var catId = await SeedCategoryAsync(db);
|
||||
var svc = new GivingService(db);
|
||||
await svc.CreateAsync(new CreateGivingRequest { GivingCategoryId = catId, Amount = 10m, PaymentMethod = "Cash", GivingDate = new DateOnly(2026,5,31) });
|
||||
|
||||
var page = await svc.GetPagedAsync(1, 20, null, catId, null, null);
|
||||
|
||||
Assert.Equal(1, page.TotalCount);
|
||||
Assert.Equal("Tithe", page.Items[0].CategoryName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_Throws_WhenGivingBelongsToSubmittedSession()
|
||||
{
|
||||
using var db = BuildDb();
|
||||
var catId = await SeedCategoryAsync(db);
|
||||
var session = new OfferingSession { SessionDate = new DateOnly(2026,5,31), Status = "Submitted" };
|
||||
db.OfferingSessions.Add(session);
|
||||
await db.SaveChangesAsync();
|
||||
var giving = new Giving { GivingCategoryId = catId, Amount = 50m, PaymentMethod = "Cash",
|
||||
GivingDate = new DateOnly(2026,5,31), OfferingSessionId = session.Id };
|
||||
db.Givings.Add(giving);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var svc = new GivingService(db);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
svc.UpdateAsync(giving.Id, new UpdateGivingRequest
|
||||
{ GivingCategoryId = catId, Amount = 999m, PaymentMethod = "Cash", GivingDate = new DateOnly(2026,5,31) }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_Throws_WhenMissing()
|
||||
{
|
||||
using var db = BuildDb();
|
||||
var svc = new GivingService(db);
|
||||
await Assert.ThrowsAsync<KeyNotFoundException>(() => svc.DeleteAsync(999));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user