Files
ROLAC/API/ROLAC.API/Controllers/ExpenseCategoriesController.cs
T
Chris Chen 95fa37ebdf fix(expense): open category read to all authed users; statement lookups via FirstOrDefaultAsync
Final-review findings:
- ExpenseCategoriesController was finance-only at the class level, but the member
  self-service reimbursement form reads the category list to populate its dropdown,
  so members got 403 and could not submit. Open GET to any authenticated user;
  keep group/subcategory writes finance-only (mirrors MinistriesController).
  Verified live with a member-role account: reads 200, writes 403, self-submit 200.
- MonthlyStatementService Update/Finalize now use FirstOrDefaultAsync for
  convention consistency with the rest of the service layer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 19:14:18 -07:00

51 lines
2.4 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.DTOs.Expense;
using ROLAC.API.Services;
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;
public ExpenseCategoriesController(IExpenseCategoryService svc) => _svc = svc;
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] bool includeInactive = false)
=> Ok(await _svc.GetAllAsync(includeInactive));
[HttpPost("groups")]
[Authorize(Roles = "finance,super_admin")]
public async Task<IActionResult> CreateGroup([FromBody] CreateExpenseGroupRequest r)
=> Ok(new { id = await _svc.CreateGroupAsync(r) });
[HttpPut("groups/{id:int}")]
[Authorize(Roles = "finance,super_admin")]
public async Task<IActionResult> UpdateGroup(int id, [FromBody] UpdateExpenseGroupRequest r)
{ try { await _svc.UpdateGroupAsync(id, r); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } }
[HttpDelete("groups/{id:int}")]
[Authorize(Roles = "finance,super_admin")]
public async Task<IActionResult> DeactivateGroup(int id)
{ try { await _svc.DeactivateGroupAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } }
[HttpPost("subcategories")]
[Authorize(Roles = "finance,super_admin")]
public async Task<IActionResult> CreateSub([FromBody] CreateExpenseSubCategoryRequest r)
{ try { return Ok(new { id = await _svc.CreateSubCategoryAsync(r) }); } catch (KeyNotFoundException) { return NotFound(); } }
[HttpPut("subcategories/{id:int}")]
[Authorize(Roles = "finance,super_admin")]
public async Task<IActionResult> UpdateSub(int id, [FromBody] UpdateExpenseSubCategoryRequest r)
{ try { await _svc.UpdateSubCategoryAsync(id, r); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } }
[HttpDelete("subcategories/{id:int}")]
[Authorize(Roles = "finance,super_admin")]
public async Task<IActionResult> DeactivateSub(int id)
{ try { await _svc.DeactivateSubCategoryAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } }
}