From f6f06d841c5e007482bd6682326b10458f75d2db Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Fri, 29 May 2026 18:03:28 -0700 Subject: [PATCH] feat(ministry): add Ministry entity, seed (10), and read endpoint --- .../Services/MinistryServiceTests.cs | 44 +++++++++++++++++++ .../Controllers/MinistriesController.cs | 18 ++++++++ API/ROLAC.API/DTOs/Ministry/MinistryDto.cs | 10 +++++ API/ROLAC.API/Data/AppDbContext.cs | 8 ++++ API/ROLAC.API/Data/DbSeeder.cs | 25 +++++++++++ API/ROLAC.API/Entities/Ministry.cs | 12 +++++ API/ROLAC.API/Program.cs | 1 + API/ROLAC.API/Services/IMinistryService.cs | 7 +++ API/ROLAC.API/Services/MinistryService.cs | 25 +++++++++++ 9 files changed, 150 insertions(+) create mode 100644 API/ROLAC.API.Tests/Services/MinistryServiceTests.cs create mode 100644 API/ROLAC.API/Controllers/MinistriesController.cs create mode 100644 API/ROLAC.API/DTOs/Ministry/MinistryDto.cs create mode 100644 API/ROLAC.API/Entities/Ministry.cs create mode 100644 API/ROLAC.API/Services/IMinistryService.cs create mode 100644 API/ROLAC.API/Services/MinistryService.cs diff --git a/API/ROLAC.API.Tests/Services/MinistryServiceTests.cs b/API/ROLAC.API.Tests/Services/MinistryServiceTests.cs new file mode 100644 index 0000000..184f686 --- /dev/null +++ b/API/ROLAC.API.Tests/Services/MinistryServiceTests.cs @@ -0,0 +1,44 @@ +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.Entities; +using ROLAC.API.Services; +using Xunit; + +namespace ROLAC.API.Tests.Services; + +public class MinistryServiceTests +{ + 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(mock.Object)).Options); + } + + [Fact] + public async Task GetAllAsync_OrdersBySortOrder_AndExcludesInactive() + { + using var db = BuildDb(); + db.Ministries.AddRange( + new Ministry { Name_en = "B", SortOrder = 2, IsActive = true }, + new Ministry { Name_en = "A", SortOrder = 1, IsActive = true }, + new Ministry { Name_en = "Z", SortOrder = 3, IsActive = false }); + await db.SaveChangesAsync(); + var svc = new MinistryService(db); + + var active = await svc.GetAllAsync(includeInactive: false); + var all = await svc.GetAllAsync(includeInactive: true); + + Assert.Equal(2, active.Count); + Assert.Equal("A", active[0].Name_en); + Assert.Equal(3, all.Count); + } +} diff --git a/API/ROLAC.API/Controllers/MinistriesController.cs b/API/ROLAC.API/Controllers/MinistriesController.cs new file mode 100644 index 0000000..e0f665a --- /dev/null +++ b/API/ROLAC.API/Controllers/MinistriesController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ROLAC.API.Services; + +namespace ROLAC.API.Controllers; + +[ApiController] +[Route("api/ministries")] +[Authorize] +public class MinistriesController : ControllerBase +{ + private readonly IMinistryService _svc; + public MinistriesController(IMinistryService svc) => _svc = svc; + + [HttpGet] + public async Task GetAll([FromQuery] bool includeInactive = false) + => Ok(await _svc.GetAllAsync(includeInactive)); +} diff --git a/API/ROLAC.API/DTOs/Ministry/MinistryDto.cs b/API/ROLAC.API/DTOs/Ministry/MinistryDto.cs new file mode 100644 index 0000000..a8ebe30 --- /dev/null +++ b/API/ROLAC.API/DTOs/Ministry/MinistryDto.cs @@ -0,0 +1,10 @@ +namespace ROLAC.API.DTOs.Ministry; + +public class MinistryDto +{ + public int Id { get; set; } + public string Name_en { get; set; } = ""; + public string? Name_zh { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } +} diff --git a/API/ROLAC.API/Data/AppDbContext.cs b/API/ROLAC.API/Data/AppDbContext.cs index f82bd04..b070ac0 100644 --- a/API/ROLAC.API/Data/AppDbContext.cs +++ b/API/ROLAC.API/Data/AppDbContext.cs @@ -14,6 +14,7 @@ public class AppDbContext : IdentityDbContext public DbSet GivingCategories => Set(); public DbSet OfferingSessions => Set(); public DbSet Givings => Set(); + public DbSet Ministries => Set(); protected override void OnModelCreating(ModelBuilder builder) { @@ -142,5 +143,12 @@ public class AppDbContext : IdentityDbContext entity.HasOne(e => e.OfferingSession).WithMany(s => s.Givings) .HasForeignKey(e => e.OfferingSessionId).OnDelete(DeleteBehavior.Cascade); }); + + // ── Ministry ───────────────────────────────────────────────────────── + builder.Entity(entity => + { + entity.Property(e => e.Name_en).HasMaxLength(200).IsRequired(); + entity.Property(e => e.Name_zh).HasMaxLength(200); + }); } } diff --git a/API/ROLAC.API/Data/DbSeeder.cs b/API/ROLAC.API/Data/DbSeeder.cs index 4dddc42..2924010 100644 --- a/API/ROLAC.API/Data/DbSeeder.cs +++ b/API/ROLAC.API/Data/DbSeeder.cs @@ -15,6 +15,20 @@ public static class DbSeeder ("Mission", "宣教奉獻", 5), ]; + private static readonly (string En, string Zh, int Sort)[] MinistrySeed = + [ + ("Administration", "行政", 1), + ("Preaching", "講道", 2), + ("Emcee", "司會", 3), + ("Worship", "敬拜", 4), + ("PPT/Media", "PPT/影音", 5), + ("Sound", "音控", 6), + ("Facility", "場地組", 7), + ("Hospitality", "招待", 8), + ("Children", "兒牧", 9), + ("Catering", "餐飲", 10), + ]; + private static readonly (string Name, string Description)[] Roles = [ ("super_admin", "System administrator — full access"), @@ -66,6 +80,16 @@ public static class DbSeeder await db.SaveChangesAsync(); } + public static async Task SeedMinistriesAsync(AppDbContext db) + { + foreach (var (en, zh, sort) in MinistrySeed) + { + if (!await db.Ministries.AnyAsync(m => m.Name_en == en)) + db.Ministries.Add(new Ministry { Name_en = en, Name_zh = zh, SortOrder = sort, IsActive = true }); + } + await db.SaveChangesAsync(); + } + /// /// Seeds roles and (in Development) the default admin account. /// Called once on application startup after migrations have been applied. @@ -80,6 +104,7 @@ public static class DbSeeder var db = services.GetRequiredService(); await SeedGivingCategoriesAsync(db); + await SeedMinistriesAsync(db); if (env.IsDevelopment()) await SeedAdminUserAsync(userManager); diff --git a/API/ROLAC.API/Entities/Ministry.cs b/API/ROLAC.API/Entities/Ministry.cs new file mode 100644 index 0000000..461fab4 --- /dev/null +++ b/API/ROLAC.API/Entities/Ministry.cs @@ -0,0 +1,12 @@ +namespace ROLAC.API.Entities; + +public class Ministry +{ + public int Id { get; set; } + public string Name_en { get; set; } = null!; + public string? Name_zh { get; set; } + public string? Description_en { get; set; } + public string? Description_zh { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } = true; +} diff --git a/API/ROLAC.API/Program.cs b/API/ROLAC.API/Program.cs index ab80cb9..916cf2a 100644 --- a/API/ROLAC.API/Program.cs +++ b/API/ROLAC.API/Program.cs @@ -121,6 +121,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/IMinistryService.cs b/API/ROLAC.API/Services/IMinistryService.cs new file mode 100644 index 0000000..cf6586e --- /dev/null +++ b/API/ROLAC.API/Services/IMinistryService.cs @@ -0,0 +1,7 @@ +using ROLAC.API.DTOs.Ministry; +namespace ROLAC.API.Services; + +public interface IMinistryService +{ + Task> GetAllAsync(bool includeInactive); +} diff --git a/API/ROLAC.API/Services/MinistryService.cs b/API/ROLAC.API/Services/MinistryService.cs new file mode 100644 index 0000000..b5d4992 --- /dev/null +++ b/API/ROLAC.API/Services/MinistryService.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using ROLAC.API.Data; +using ROLAC.API.DTOs.Ministry; + +namespace ROLAC.API.Services; + +public class MinistryService : IMinistryService +{ + private readonly AppDbContext _db; + public MinistryService(AppDbContext db) => _db = db; + + public async Task> GetAllAsync(bool includeInactive) + { + var query = _db.Ministries.AsNoTracking().AsQueryable(); + if (!includeInactive) query = query.Where(m => m.IsActive); + return await query + .OrderBy(m => m.SortOrder).ThenBy(m => m.Name_en) + .Select(m => new MinistryDto + { + Id = m.Id, Name_en = m.Name_en, Name_zh = m.Name_zh, + SortOrder = m.SortOrder, IsActive = m.IsActive, + }) + .ToListAsync(); + } +}