Add 4 AI bookkeeping features
Feature 7: Bank Rec Auto-Match — AiSuggestMatches endpoint scores uncleared transactions vs statement ending balance; AI Auto-Match panel in Reconcile.cshtml with confidence highlights and Apply All button. Feature 8: Late Payment Prediction — PredictLatePayments endpoint scores open AR customers by risk (high/medium/low) using historical avg-days-to-pay + late rate; rendered as badge table in AR Aging view via ar-aging-ai.js. Feature 9: Natural Language Financial Queries — FinancialQuery GET page + RunFinancialQuery POST; 12-month context snapshot pre-loaded; answers grounded in real data with supporting facts, follow-up suggestions, session history, and example chips. Feature 10: Recurring Bill Detection — RunRecurringDetection scans 12 months of bills for vendor payment patterns (monthly/quarterly/annual); card grid view in Bills/RecurringDetection.cshtml with confidence badges, next-expected-date, and suggested actions. Supporting: 4 new DTO groups in AccountingAiDtos.cs, 4 method signatures in IAccountingAiService.cs, 4 implementations in AccountingAiService.cs, 4 new AiFeatures constants, 2 new Landing page AI report cards. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Application.DTOs.AI;
|
||||
using PowderCoating.Application.Interfaces;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
@@ -15,13 +16,19 @@ public class BankReconciliationsController : Controller
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly IAccountingAiService _accountingAi;
|
||||
private readonly IAiUsageLogger _usageLogger;
|
||||
|
||||
public BankReconciliationsController(
|
||||
IUnitOfWork unitOfWork,
|
||||
ITenantContext tenantContext)
|
||||
ITenantContext tenantContext,
|
||||
IAccountingAiService accountingAi,
|
||||
IAiUsageLogger usageLogger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tenantContext = tenantContext;
|
||||
_accountingAi = accountingAi;
|
||||
_usageLogger = usageLogger;
|
||||
}
|
||||
|
||||
private bool AllowAccounting() =>
|
||||
@@ -269,6 +276,91 @@ public class BankReconciliationsController : Controller
|
||||
return View(recon);
|
||||
}
|
||||
|
||||
// ── AI Auto-Match (AJAX) ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// AJAX endpoint. Passes uncleared bank rec items to Claude and returns suggested items
|
||||
/// to mark as cleared. The controller assembles all three transaction types (deposits,
|
||||
/// bill payments, expenses) for the reconciliation's account, then delegates scoring to
|
||||
/// <see cref="IAccountingAiService.AutoMatchReconciliationAsync"/>. The caller applies
|
||||
/// suggestions client-side by auto-checking the corresponding table rows.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize(Policy = AppConstants.Policies.CanManageJobs)]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> AiSuggestMatches(int reconId)
|
||||
{
|
||||
if (!AllowAccounting()) return Forbid();
|
||||
|
||||
var recon = (await _unitOfWork.BankReconciliations.FindAsync(
|
||||
br => br.Id == reconId, false, br => br.Account))
|
||||
.FirstOrDefault();
|
||||
if (recon == null) return NotFound();
|
||||
|
||||
var accountId = recon.AccountId;
|
||||
var statementDate = recon.StatementDate;
|
||||
|
||||
var items = new List<BankRecMatchItem>();
|
||||
|
||||
(await _unitOfWork.Payments.FindAsync(
|
||||
p => p.DepositAccountId == accountId && p.PaymentDate <= statementDate && !p.IsCleared))
|
||||
.ToList()
|
||||
.ForEach(p => items.Add(new BankRecMatchItem
|
||||
{
|
||||
EntityType = "Payment",
|
||||
EntityId = p.Id,
|
||||
Date = p.PaymentDate.ToString("yyyy-MM-dd"),
|
||||
Reference = p.Reference ?? $"PMT-{p.Id}",
|
||||
Description = $"Payment #{p.InvoiceId}",
|
||||
Amount = p.Amount,
|
||||
Direction = "deposit"
|
||||
}));
|
||||
|
||||
(await _unitOfWork.BillPayments.FindAsync(
|
||||
bp => bp.BankAccountId == accountId && bp.PaymentDate <= statementDate && !bp.IsCleared))
|
||||
.ToList()
|
||||
.ForEach(bp => items.Add(new BankRecMatchItem
|
||||
{
|
||||
EntityType = "BillPayment",
|
||||
EntityId = bp.Id,
|
||||
Date = bp.PaymentDate.ToString("yyyy-MM-dd"),
|
||||
Reference = bp.PaymentNumber,
|
||||
Description = bp.Memo ?? bp.BillId.ToString(),
|
||||
Amount = bp.Amount,
|
||||
Direction = "payment"
|
||||
}));
|
||||
|
||||
(await _unitOfWork.Expenses.FindAsync(
|
||||
e => e.PaymentAccountId == accountId && e.Date <= statementDate && !e.IsCleared))
|
||||
.ToList()
|
||||
.ForEach(e => items.Add(new BankRecMatchItem
|
||||
{
|
||||
EntityType = "Expense",
|
||||
EntityId = e.Id,
|
||||
Date = e.Date.ToString("yyyy-MM-dd"),
|
||||
Reference = e.ExpenseNumber,
|
||||
Description = e.Memo ?? string.Empty,
|
||||
Amount = e.Amount,
|
||||
Direction = "payment"
|
||||
}));
|
||||
|
||||
if (!items.Any())
|
||||
return Json(new { success = false, errorMessage = "No uncleared transactions to analyze." });
|
||||
|
||||
var request = new AutoMatchRequest
|
||||
{
|
||||
UnclearedItems = items,
|
||||
BeginningBalance = recon.BeginningBalance,
|
||||
StatementEndingBalance = recon.EndingBalance
|
||||
};
|
||||
|
||||
var result = await _accountingAi.AutoMatchReconciliationAsync(request);
|
||||
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "";
|
||||
await _usageLogger.LogAsync(recon.CompanyId, userId, AppConstants.AiFeatures.BankRecAutoMatch, result.Success);
|
||||
|
||||
return Json(result);
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private async Task PopulateAccountDropdownAsync()
|
||||
|
||||
Reference in New Issue
Block a user