Add Balance Reconciliation report (detective control)

Phase 1 of the accounting audit remediation. A read-only diagnostic that surfaces
drift in the denormalized balances without changing any posting:

- Per account: stored Account.CurrentBalance vs the balance recomputed from source
  documents (the same LedgerService path RecalculateBalances uses). Drifted rows
  are highlighted; a difference means the cache is stale and a recalc would fix it.
- AR subledger (sum of Customer.CurrentBalance) vs the AR control account, and AP
  subledger (sum of Vendor.CurrentBalance) vs the AP control account.

FinancialReportService now takes ILedgerService to recompute. New
GetBalanceReconciliationAsync + BalanceReconciliationDto, a /Reports/Reconciliation
action, view, and a card on the reports landing. Build clean; 284 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 09:05:16 -04:00
parent 9ce361235f
commit c2cd19e475
6 changed files with 232 additions and 1 deletions
@@ -1250,6 +1250,20 @@ public class ReportsController : Controller
return View(dto);
}
/// <summary>
/// Balance reconciliation diagnostic: each account's stored CurrentBalance vs its recomputed ledger
/// balance, plus AR/AP subledger totals vs their control accounts. Read-only; surfaces drift in the
/// denormalized balances without changing any posting. Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/Reconciliation
public async Task<IActionResult> Reconciliation()
{
if (!AllowAccounting()) return RedirectToAction(nameof(Landing));
var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0;
var dto = await _financialReports.GetBalanceReconciliationAsync(companyId);
return View(dto);
}
/// <summary>
/// PDF export of the Trial Balance report. Same inline/attachment pattern as other PDF actions.
/// Gated behind <see cref="AllowAccounting"/>.