using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using ROLAC.API.Authorization; using ROLAC.API.DTOs.Expense; using ROLAC.API.Services; using ROLAC.API.Services.Ai; namespace ROLAC.API.Controllers; [ApiController] [Route("api/expense-categories")] [Authorize] // read (GetAll) is open to any authenticated user — the member self-service // reimbursement form needs the category list. Write actions are finance-only below. public class ExpenseCategoriesController : ControllerBase { private readonly IExpenseCategoryService _svc; private readonly IExpenseCategoryAiServiceFactory _aiFactory; public ExpenseCategoriesController(IExpenseCategoryService svc, IExpenseCategoryAiServiceFactory aiFactory) { _svc = svc; _aiFactory = aiFactory; } [HttpGet] public async Task GetAll([FromQuery] bool includeInactive = false) => Ok(await _svc.GetAllAsync(includeInactive)); // Suggest an English name + Form 990 line for a category being defined. Write-gated: category // editing is finance/admin-only, unlike the member-facing expense-ai/assist endpoint. [HttpPost("ai-suggest")] [HasPermission(Modules.ExpenseCategories, PermissionActions.Write)] public async Task AiSuggest([FromBody] ExpenseCategoryAiRequest r, CancellationToken ct) { if (string.IsNullOrWhiteSpace(r.Name_zh) && string.IsNullOrWhiteSpace(r.Name_en)) return BadRequest("A name is required."); var svc = await _aiFactory.ResolveAsync(ct); return Ok(await svc.SuggestAsync(r, ct)); } [HttpPost("groups")] [HasPermission(Modules.ExpenseCategories, PermissionActions.Write)] public async Task CreateGroup([FromBody] CreateExpenseGroupRequest r) => Ok(new { id = await _svc.CreateGroupAsync(r) }); [HttpPut("groups/{id:int}")] [HasPermission(Modules.ExpenseCategories, PermissionActions.Write)] public async Task UpdateGroup(int id, [FromBody] UpdateExpenseGroupRequest r) { try { await _svc.UpdateGroupAsync(id, r); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } } [HttpDelete("groups/{id:int}")] [HasPermission(Modules.ExpenseCategories, PermissionActions.Delete)] public async Task DeactivateGroup(int id) { try { await _svc.DeactivateGroupAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } } [HttpPost("subcategories")] [HasPermission(Modules.ExpenseCategories, PermissionActions.Write)] public async Task CreateSub([FromBody] CreateExpenseSubCategoryRequest r) { try { return Ok(new { id = await _svc.CreateSubCategoryAsync(r) }); } catch (KeyNotFoundException) { return NotFound(); } } [HttpPut("subcategories/{id:int}")] [HasPermission(Modules.ExpenseCategories, PermissionActions.Write)] public async Task UpdateSub(int id, [FromBody] UpdateExpenseSubCategoryRequest r) { try { await _svc.UpdateSubCategoryAsync(id, r); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } } [HttpDelete("subcategories/{id:int}")] [HasPermission(Modules.ExpenseCategories, PermissionActions.Delete)] public async Task DeactivateSub(int id) { try { await _svc.DeactivateSubCategoryAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } } }