feat(ministry): add Ministry entity, seed (10), and read endpoint
This commit is contained in:
@@ -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<IHttpContextAccessor>();
|
||||||
|
mock.Setup(x => x.HttpContext).Returns(ctx);
|
||||||
|
return new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<IActionResult> GetAll([FromQuery] bool includeInactive = false)
|
||||||
|
=> Ok(await _svc.GetAllAsync(includeInactive));
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
|
|||||||
public DbSet<GivingCategory> GivingCategories => Set<GivingCategory>();
|
public DbSet<GivingCategory> GivingCategories => Set<GivingCategory>();
|
||||||
public DbSet<OfferingSession> OfferingSessions => Set<OfferingSession>();
|
public DbSet<OfferingSession> OfferingSessions => Set<OfferingSession>();
|
||||||
public DbSet<Giving> Givings => Set<Giving>();
|
public DbSet<Giving> Givings => Set<Giving>();
|
||||||
|
public DbSet<Ministry> Ministries => Set<Ministry>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
@@ -142,5 +143,12 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
|
|||||||
entity.HasOne(e => e.OfferingSession).WithMany(s => s.Givings)
|
entity.HasOne(e => e.OfferingSession).WithMany(s => s.Givings)
|
||||||
.HasForeignKey(e => e.OfferingSessionId).OnDelete(DeleteBehavior.Cascade);
|
.HasForeignKey(e => e.OfferingSessionId).OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Ministry ─────────────────────────────────────────────────────────
|
||||||
|
builder.Entity<Ministry>(entity =>
|
||||||
|
{
|
||||||
|
entity.Property(e => e.Name_en).HasMaxLength(200).IsRequired();
|
||||||
|
entity.Property(e => e.Name_zh).HasMaxLength(200);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,20 @@ public static class DbSeeder
|
|||||||
("Mission", "宣教奉獻", 5),
|
("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 =
|
private static readonly (string Name, string Description)[] Roles =
|
||||||
[
|
[
|
||||||
("super_admin", "System administrator — full access"),
|
("super_admin", "System administrator — full access"),
|
||||||
@@ -66,6 +80,16 @@ public static class DbSeeder
|
|||||||
await db.SaveChangesAsync();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seeds roles and (in Development) the default admin account.
|
/// Seeds roles and (in Development) the default admin account.
|
||||||
/// Called once on application startup after migrations have been applied.
|
/// Called once on application startup after migrations have been applied.
|
||||||
@@ -80,6 +104,7 @@ public static class DbSeeder
|
|||||||
|
|
||||||
var db = services.GetRequiredService<AppDbContext>();
|
var db = services.GetRequiredService<AppDbContext>();
|
||||||
await SeedGivingCategoriesAsync(db);
|
await SeedGivingCategoriesAsync(db);
|
||||||
|
await SeedMinistriesAsync(db);
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
await SeedAdminUserAsync(userManager);
|
await SeedAdminUserAsync(userManager);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -121,6 +121,7 @@ builder.Services.AddScoped<IUserManagementService, UserManagementService>();
|
|||||||
builder.Services.AddScoped<IGivingCategoryService, GivingCategoryService>();
|
builder.Services.AddScoped<IGivingCategoryService, GivingCategoryService>();
|
||||||
builder.Services.AddScoped<IGivingService, GivingService>();
|
builder.Services.AddScoped<IGivingService, GivingService>();
|
||||||
builder.Services.AddScoped<IOfferingSessionService, OfferingSessionService>();
|
builder.Services.AddScoped<IOfferingSessionService, OfferingSessionService>();
|
||||||
|
builder.Services.AddScoped<IMinistryService, MinistryService>();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Swagger / MVC
|
// Swagger / MVC
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ROLAC.API.DTOs.Ministry;
|
||||||
|
namespace ROLAC.API.Services;
|
||||||
|
|
||||||
|
public interface IMinistryService
|
||||||
|
{
|
||||||
|
Task<List<MinistryDto>> GetAllAsync(bool includeInactive);
|
||||||
|
}
|
||||||
@@ -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<List<MinistryDto>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user