73077295a4
ci-cd-vm / ci-cd (push) Successful in 2m25s
Add an AI assist button to the Edit/New Group (大項) and Subcategory (小項) dialogs: the user enters a Chinese name, and the model refines the Chinese, translates it to English, and suggests the matching IRS Form 990 Part IX line. Suggestions surface in a confirm card; Apply fills the Chinese name, English name, and 990 line fields. Backend mirrors the existing expense-classification AI family but over the Form 990 line catalog: IExpenseCategoryAiService + base (catalog load, prompt, id validation) + Claude/Gemini providers + factory that picks the provider from ChurchProfile.AiProvider. New write-gated POST api/expense-categories/ai-suggest endpoint; sub-category requests pass the parent group + its 990 line to bias the choice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
3.4 KiB
C#
71 lines
3.4 KiB
C#
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<IActionResult> 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<IActionResult> 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<IActionResult> CreateGroup([FromBody] CreateExpenseGroupRequest r)
|
|
=> Ok(new { id = await _svc.CreateGroupAsync(r) });
|
|
|
|
[HttpPut("groups/{id:int}")]
|
|
[HasPermission(Modules.ExpenseCategories, PermissionActions.Write)]
|
|
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}")]
|
|
[HasPermission(Modules.ExpenseCategories, PermissionActions.Delete)]
|
|
public async Task<IActionResult> DeactivateGroup(int id)
|
|
{ try { await _svc.DeactivateGroupAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } }
|
|
|
|
[HttpPost("subcategories")]
|
|
[HasPermission(Modules.ExpenseCategories, PermissionActions.Write)]
|
|
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}")]
|
|
[HasPermission(Modules.ExpenseCategories, PermissionActions.Write)]
|
|
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}")]
|
|
[HasPermission(Modules.ExpenseCategories, PermissionActions.Delete)]
|
|
public async Task<IActionResult> DeactivateSub(int id)
|
|
{ try { await _svc.DeactivateSubCategoryAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } }
|
|
}
|