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>
70 lines
2.6 KiB
C#
70 lines
2.6 KiB
C#
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Moq;
|
|
using ROLAC.API.Data;
|
|
using ROLAC.API.Data.Interceptors;
|
|
using ROLAC.API.Entities;
|
|
using ROLAC.API.Services.Ai;
|
|
using ROLAC.API.Services.Logging;
|
|
using Xunit;
|
|
|
|
namespace ROLAC.API.Tests.Services;
|
|
|
|
public class ExpenseCategoryAiServiceFactoryTests
|
|
{
|
|
// ChurchProfile is auditable, so the InMemory store rejects saves unless the
|
|
// required CreatedBy/UpdatedBy fields are populated. Wire the same audit
|
|
// interceptor the app uses so seeded entities save cleanly.
|
|
private static AppDbContext NewDb()
|
|
{
|
|
var httpContext = new DefaultHttpContext
|
|
{
|
|
User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "test-user") })),
|
|
};
|
|
var httpContextAccessor = new Mock<IHttpContextAccessor>();
|
|
httpContextAccessor.Setup(accessor => accessor.HttpContext).Returns(httpContext);
|
|
return new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.ConfigureWarnings(warnings => warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning))
|
|
.AddInterceptors(new AuditSaveChangesInterceptor(new CurrentUserAccessor(httpContextAccessor.Object)))
|
|
.Options);
|
|
}
|
|
|
|
private static ExpenseCategoryAiServiceFactory Build(AppDbContext db)
|
|
{
|
|
var cfg = new ChurchAiConfigProvider(db);
|
|
var claude = new ClaudeExpenseCategoryAiService(
|
|
new HttpClient(), cfg, db, NullLogger<ClaudeExpenseCategoryAiService>.Instance);
|
|
var gemini = new GeminiExpenseCategoryAiService(
|
|
new HttpClient(), cfg, db, NullLogger<GeminiExpenseCategoryAiService>.Instance);
|
|
return new ExpenseCategoryAiServiceFactory(cfg, claude, gemini);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Resolves_Claude_by_default()
|
|
{
|
|
using var db = NewDb();
|
|
db.ChurchProfiles.Add(new ChurchProfile { Name = "C", AiProvider = "Claude" });
|
|
await db.SaveChangesAsync();
|
|
|
|
var svc = await Build(db).ResolveAsync();
|
|
|
|
Assert.IsType<ClaudeExpenseCategoryAiService>(svc);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Resolves_Gemini_when_selected()
|
|
{
|
|
using var db = NewDb();
|
|
db.ChurchProfiles.Add(new ChurchProfile { Name = "C", AiProvider = "Gemini" });
|
|
await db.SaveChangesAsync();
|
|
|
|
var svc = await Build(db).ResolveAsync();
|
|
|
|
Assert.IsType<GeminiExpenseCategoryAiService>(svc);
|
|
}
|
|
}
|