diff --git a/API/ROLAC.API.Tests/Services/GivingCategoryServiceTests.cs b/API/ROLAC.API.Tests/Services/GivingCategoryServiceTests.cs new file mode 100644 index 0000000..6abdee5 --- /dev/null +++ b/API/ROLAC.API.Tests/Services/GivingCategoryServiceTests.cs @@ -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(); + 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() + .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(() => + svc.UpdateAsync(999, new UpdateGivingCategoryRequest { Name_en = "X" })); + } +} diff --git a/API/ROLAC.API/DTOs/Giving/CreateGivingCategoryRequest.cs b/API/ROLAC.API/DTOs/Giving/CreateGivingCategoryRequest.cs new file mode 100644 index 0000000..6fa94de --- /dev/null +++ b/API/ROLAC.API/DTOs/Giving/CreateGivingCategoryRequest.cs @@ -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; } +} diff --git a/API/ROLAC.API/DTOs/Giving/GivingCategoryDto.cs b/API/ROLAC.API/DTOs/Giving/GivingCategoryDto.cs new file mode 100644 index 0000000..3b1b612 --- /dev/null +++ b/API/ROLAC.API/DTOs/Giving/GivingCategoryDto.cs @@ -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; } +} diff --git a/API/ROLAC.API/DTOs/Giving/UpdateGivingCategoryRequest.cs b/API/ROLAC.API/DTOs/Giving/UpdateGivingCategoryRequest.cs new file mode 100644 index 0000000..84ded3a --- /dev/null +++ b/API/ROLAC.API/DTOs/Giving/UpdateGivingCategoryRequest.cs @@ -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; } +} diff --git a/API/ROLAC.API/Program.cs b/API/ROLAC.API/Program.cs index c5086fb..95b4dea 100644 --- a/API/ROLAC.API/Program.cs +++ b/API/ROLAC.API/Program.cs @@ -118,6 +118,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // --------------------------------------------------------------------------- // Swagger / MVC diff --git a/API/ROLAC.API/Services/GivingCategoryService.cs b/API/ROLAC.API/Services/GivingCategoryService.cs new file mode 100644 index 0000000..4689957 --- /dev/null +++ b/API/ROLAC.API/Services/GivingCategoryService.cs @@ -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> 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 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(); + } +} diff --git a/API/ROLAC.API/Services/IGivingCategoryService.cs b/API/ROLAC.API/Services/IGivingCategoryService.cs new file mode 100644 index 0000000..6a67372 --- /dev/null +++ b/API/ROLAC.API/Services/IGivingCategoryService.cs @@ -0,0 +1,11 @@ +using ROLAC.API.DTOs.Giving; + +namespace ROLAC.API.Services; + +public interface IGivingCategoryService +{ + Task> GetAllAsync(bool includeInactive); + Task CreateAsync(CreateGivingCategoryRequest request); + Task UpdateAsync(int id, UpdateGivingCategoryRequest request); + Task DeactivateAsync(int id); // soft-disable: IsActive = false +}