feat(giving): giving-category service with CRUD + soft-disable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
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.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ROLAC.API.Tests.Services;
|
||||||
|
|
||||||
|
public class GivingCategoryServiceTests
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAsync_ReturnsId_AndDefaultsActive()
|
||||||
|
{
|
||||||
|
using var db = BuildDb();
|
||||||
|
var svc = new GivingCategoryService(db, BuildAccessor());
|
||||||
|
|
||||||
|
var id = await svc.CreateAsync(new CreateGivingCategoryRequest { Name_en = "Tithe", Name_zh = "什一" });
|
||||||
|
|
||||||
|
var saved = await db.GivingCategories.FindAsync(id);
|
||||||
|
Assert.NotNull(saved);
|
||||||
|
Assert.True(saved!.IsActive);
|
||||||
|
Assert.Equal("Tithe", saved.Name_en);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAllAsync_ExcludesInactive_ByDefault()
|
||||||
|
{
|
||||||
|
using var db = BuildDb();
|
||||||
|
var svc = new GivingCategoryService(db, BuildAccessor());
|
||||||
|
var id1 = await svc.CreateAsync(new CreateGivingCategoryRequest { Name_en = "Active" });
|
||||||
|
var id2 = await svc.CreateAsync(new CreateGivingCategoryRequest { Name_en = "Gone" });
|
||||||
|
await svc.DeactivateAsync(id2);
|
||||||
|
|
||||||
|
var active = await svc.GetAllAsync(includeInactive: false);
|
||||||
|
var all = await svc.GetAllAsync(includeInactive: true);
|
||||||
|
|
||||||
|
Assert.Single(active);
|
||||||
|
Assert.Equal(2, all.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeactivateAsync_SetsIsActiveFalse()
|
||||||
|
{
|
||||||
|
using var db = BuildDb();
|
||||||
|
var svc = new GivingCategoryService(db, BuildAccessor());
|
||||||
|
var id = await svc.CreateAsync(new CreateGivingCategoryRequest { Name_en = "Temp" });
|
||||||
|
|
||||||
|
await svc.DeactivateAsync(id);
|
||||||
|
|
||||||
|
var saved = await db.GivingCategories.FindAsync(id);
|
||||||
|
Assert.False(saved!.IsActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_Throws_WhenMissing()
|
||||||
|
{
|
||||||
|
using var db = BuildDb();
|
||||||
|
var svc = new GivingCategoryService(db, BuildAccessor());
|
||||||
|
await Assert.ThrowsAsync<KeyNotFoundException>(() =>
|
||||||
|
svc.UpdateAsync(999, new UpdateGivingCategoryRequest { Name_en = "X" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
namespace ROLAC.API.DTOs.Giving;
|
||||||
|
|
||||||
|
public class CreateGivingCategoryRequest
|
||||||
|
{
|
||||||
|
[Required, MaxLength(200)] public string Name_en { get; set; } = "";
|
||||||
|
[MaxLength(200)] public string? Name_zh { get; set; }
|
||||||
|
[MaxLength(500)] public string? Description_en { get; set; }
|
||||||
|
[MaxLength(500)] public string? Description_zh { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace ROLAC.API.DTOs.Giving;
|
||||||
|
|
||||||
|
public class GivingCategoryDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name_en { get; set; } = "";
|
||||||
|
public string? Name_zh { get; set; }
|
||||||
|
public string? Description_en { get; set; }
|
||||||
|
public string? Description_zh { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
namespace ROLAC.API.DTOs.Giving;
|
||||||
|
|
||||||
|
public class UpdateGivingCategoryRequest
|
||||||
|
{
|
||||||
|
[Required, MaxLength(200)] public string Name_en { get; set; } = "";
|
||||||
|
[MaxLength(200)] public string? Name_zh { get; set; }
|
||||||
|
[MaxLength(500)] public string? Description_en { get; set; }
|
||||||
|
[MaxLength(500)] public string? Description_zh { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@@ -118,6 +118,7 @@ builder.Services.AddScoped<ITokenService, TokenService>();
|
|||||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||||
builder.Services.AddScoped<IMemberService, MemberService>();
|
builder.Services.AddScoped<IMemberService, MemberService>();
|
||||||
builder.Services.AddScoped<IUserManagementService, UserManagementService>();
|
builder.Services.AddScoped<IUserManagementService, UserManagementService>();
|
||||||
|
builder.Services.AddScoped<IGivingCategoryService, GivingCategoryService>();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Swagger / MVC
|
// Swagger / MVC
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using ROLAC.API.Data;
|
||||||
|
using ROLAC.API.DTOs.Giving;
|
||||||
|
using ROLAC.API.Entities;
|
||||||
|
|
||||||
|
namespace ROLAC.API.Services;
|
||||||
|
|
||||||
|
public class GivingCategoryService : IGivingCategoryService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
public GivingCategoryService(AppDbContext db, IHttpContextAccessor http) => _db = db;
|
||||||
|
|
||||||
|
public async Task<List<GivingCategoryDto>> GetAllAsync(bool includeInactive)
|
||||||
|
{
|
||||||
|
var query = _db.GivingCategories.AsNoTracking().AsQueryable();
|
||||||
|
if (!includeInactive) query = query.Where(c => c.IsActive);
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.OrderBy(c => c.SortOrder).ThenBy(c => c.Name_en)
|
||||||
|
.Select(c => new GivingCategoryDto
|
||||||
|
{
|
||||||
|
Id = c.Id, Name_en = c.Name_en, Name_zh = c.Name_zh,
|
||||||
|
Description_en = c.Description_en, Description_zh = c.Description_zh,
|
||||||
|
IsActive = c.IsActive, SortOrder = c.SortOrder,
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CreateAsync(CreateGivingCategoryRequest r)
|
||||||
|
{
|
||||||
|
var entity = new GivingCategory
|
||||||
|
{
|
||||||
|
Name_en = r.Name_en, Name_zh = r.Name_zh,
|
||||||
|
Description_en = r.Description_en, Description_zh = r.Description_zh,
|
||||||
|
SortOrder = r.SortOrder, IsActive = true,
|
||||||
|
};
|
||||||
|
_db.GivingCategories.Add(entity);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return entity.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(int id, UpdateGivingCategoryRequest r)
|
||||||
|
{
|
||||||
|
var c = await _db.GivingCategories.FindAsync(id)
|
||||||
|
?? throw new KeyNotFoundException($"GivingCategory {id} not found.");
|
||||||
|
c.Name_en = r.Name_en; c.Name_zh = r.Name_zh;
|
||||||
|
c.Description_en = r.Description_en; c.Description_zh = r.Description_zh;
|
||||||
|
c.IsActive = r.IsActive; c.SortOrder = r.SortOrder;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeactivateAsync(int id)
|
||||||
|
{
|
||||||
|
var c = await _db.GivingCategories.FindAsync(id)
|
||||||
|
?? throw new KeyNotFoundException($"GivingCategory {id} not found.");
|
||||||
|
c.IsActive = false;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using ROLAC.API.DTOs.Giving;
|
||||||
|
|
||||||
|
namespace ROLAC.API.Services;
|
||||||
|
|
||||||
|
public interface IGivingCategoryService
|
||||||
|
{
|
||||||
|
Task<List<GivingCategoryDto>> GetAllAsync(bool includeInactive);
|
||||||
|
Task<int> CreateAsync(CreateGivingCategoryRequest request);
|
||||||
|
Task UpdateAsync(int id, UpdateGivingCategoryRequest request);
|
||||||
|
Task DeactivateAsync(int id); // soft-disable: IsActive = false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user