Phase E: Add Bank Reconciliation

- IsCleared + ClearedDate added to Payment, BillPayment, Expense entities
- BankReconciliation entity (account, statement date, beginning/ending balance, status)
- BankReconciliationStatus enum (InProgress, Completed)
- Migration AddBankReconciliation: new BankReconciliations table + IsCleared/ClearedDate columns
- IUnitOfWork/UnitOfWork wired with BankReconciliations repo
- BankReconciliationsController: Index, Create, Reconcile, ToggleCleared (AJAX), Complete, Report
- Reconcile view: deposit/payment checkboxes with live running balance and difference via JS
- Complete is gated: only enabled when difference == $0.00
- Nav: Bank Reconciliation added to Finance section in _Layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 00:10:38 -04:00
parent cf9dcfb4c1
commit 1229081436
15 changed files with 11111 additions and 3 deletions
@@ -122,6 +122,10 @@ public class BillPayment : BaseEntity
public string? CheckNumber { get; set; }
public string? Memo { get; set; }
/// <summary>True once this payment has been matched against a bank statement during reconciliation.</summary>
public bool IsCleared { get; set; } = false;
public DateTime? ClearedDate { get; set; }
// Navigation
public virtual Bill Bill { get; set; } = null!;
public virtual Vendor Vendor { get; set; } = null!;
@@ -150,6 +154,10 @@ public class Expense : BaseEntity
public string? Memo { get; set; }
public string? ReceiptFilePath { get; set; }
/// <summary>True once this expense has been matched against a bank statement during reconciliation.</summary>
public bool IsCleared { get; set; } = false;
public DateTime? ClearedDate { get; set; }
// Navigation
public virtual Vendor? Vendor { get; set; }
public virtual Account ExpenseAccount { get; set; } = null!;
@@ -201,6 +209,27 @@ public class JournalEntryLine : BaseEntity
public virtual Account Account { get; set; } = null!;
}
/// <summary>
/// A bank reconciliation session for a single bank/cash account against a statement.
/// Cleared balance = BeginningBalance + cleared deposits - cleared payments.
/// The reconciliation is complete when Difference (EndingBalance - ClearedBalance) == 0.
/// </summary>
public class BankReconciliation : BaseEntity
{
/// <summary>Must be a bank/cash subtype account.</summary>
public int AccountId { get; set; }
public DateTime StatementDate { get; set; }
public decimal BeginningBalance { get; set; }
public decimal EndingBalance { get; set; }
public BankReconciliationStatus Status { get; set; } = BankReconciliationStatus.InProgress;
public DateTime? CompletedAt { get; set; }
public string? CompletedBy { get; set; }
public string? Notes { get; set; }
// Navigation
public virtual Account Account { get; set; } = null!;
}
/// <summary>
/// A credit note received from a vendor (returned goods, pricing dispute, short-ship).
/// Reduces Accounts Payable and reverses the original expense/COGS when posted.
@@ -18,6 +18,10 @@ public class Payment : BaseEntity
/// </summary>
public int? DepositAccountId { get; set; }
/// <summary>True once this payment has been matched against a bank statement during reconciliation.</summary>
public bool IsCleared { get; set; } = false;
public DateTime? ClearedDate { get; set; }
// Navigation
public virtual Invoice Invoice { get; set; } = null!;
public virtual ApplicationUser? RecordedBy { get; set; }
@@ -80,6 +80,12 @@ public enum AccountingMethod
Accrual = 1
}
public enum BankReconciliationStatus
{
InProgress = 0,
Completed = 1
}
public enum VendorCreditStatus
{
Open = 0,
@@ -100,6 +100,9 @@ public interface IUnitOfWork : IDisposable
IRepository<VendorCreditLineItem> VendorCreditLineItems { get; }
IRepository<VendorCreditApplication> VendorCreditApplications { get; }
// Bank Reconciliation
IRepository<BankReconciliation> BankReconciliations { get; }
// Notifications — typed repository for IgnoreQueryFilters-based history lookups
INotificationLogRepository NotificationLogs { get; }
IRepository<NotificationTemplate> NotificationTemplates { get; }