Close all GL entry gaps across the accounting surface
- Stripe payments/refunds/chargebacks now post DR/CR entries (PaymentController) - Vendor credit void now reverses the posted GL lines (VendorCreditsController) - Gift certificate issue/redeem/void post GL to account 2500 GC Liability; FinancialReportService Trial Balance + Balance Sheet include GC liability and breakage income; P&L shows deferred revenue deduction and breakage income line - Customer deposits now post DR Checking / CR 2300 on record, reverse on delete; invoice auto-apply uses DR 2300 / CR AR (not a second bank debit); draft invoice delete reverses deposit-apply GL before the AR reversal - Deposit.DepositAccountId column added; account 2300 seeded via migration - InvoicesController.ApplyCredit now posts DR Sales Discounts / CR AR, consistent with CreditMemosController.Apply - IssueRefund (cash/card) posts DR AR / CR Bank and sets Refund.DepositAccountId; refund modal gains a bank account selector hidden for store-credit path - CancelRefund (cash/card) reverses the IssueRefund GL entries - LedgerService GetAccountLedgerAsync + ComputePriorBalanceAsync now include Refunds, CreditMemoApplications, VendorCreditApplications, GC Liability (2500), and Customer Deposits (2300) so account ledger view and RecalculateAllAsync produce correct balances - Three EF migrations applied: SeedSalesDiscountsAccount, AccountingGapsPhase2, AccountingDepositsGL - Unit tests updated for new IAccountBalanceService constructor params (200/200) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,45 @@ public class LedgerService : ILedgerService
|
||||
LinkId = p.InvoiceId
|
||||
});
|
||||
|
||||
// Customer deposits recorded to this account (DEBIT — cash received at deposit time)
|
||||
var depositedDeposits = await _context.Deposits
|
||||
.Where(d => d.DepositAccountId == accountId
|
||||
&& d.ReceivedDate >= fromDate && d.ReceivedDate <= toDate)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var d in depositedDeposits)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = d.ReceivedDate,
|
||||
Reference = d.ReceiptNumber,
|
||||
Source = "Customer Deposit",
|
||||
Description = d.Notes ?? d.Reference,
|
||||
Debit = d.Amount,
|
||||
Credit = 0,
|
||||
LinkController = "Jobs",
|
||||
LinkId = d.JobId
|
||||
});
|
||||
|
||||
// Refunds paid FROM this account (CREDIT — cash leaves)
|
||||
var refundsPaidFrom = await _context.Refunds
|
||||
.Include(r => r.Invoice)
|
||||
.Where(r => r.DepositAccountId == accountId
|
||||
&& r.RefundDate >= fromDate && r.RefundDate <= toDate)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var r in refundsPaidFrom)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = r.RefundDate,
|
||||
Reference = r.Reference ?? $"REF-{r.Id}",
|
||||
Source = "Refund",
|
||||
Description = r.Reason,
|
||||
Debit = 0,
|
||||
Credit = r.Amount,
|
||||
LinkController = "Invoices",
|
||||
LinkId = r.InvoiceId
|
||||
});
|
||||
|
||||
// ── 2. Direct expenses paid FROM this account (CREDIT) ────────────────
|
||||
// e.g. Checking account used to pay an expense
|
||||
var expensesPaidFrom = await _context.Expenses
|
||||
@@ -251,6 +290,46 @@ public class LedgerService : ILedgerService
|
||||
LinkController = "Invoices",
|
||||
LinkId = p.InvoiceId
|
||||
});
|
||||
|
||||
// Credit memo applications reduce open AR (CREDIT)
|
||||
var arCreditMemos = await _context.CreditMemoApplications
|
||||
.Include(a => a.Invoice)
|
||||
.Include(a => a.CreditMemo)
|
||||
.Where(a => a.AppliedDate >= fromDate && a.AppliedDate <= toDate
|
||||
&& a.Invoice.Status != InvoiceStatus.Voided)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var cm in arCreditMemos)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = cm.AppliedDate,
|
||||
Reference = cm.CreditMemo?.MemoNumber ?? $"CM-{cm.Id}",
|
||||
Source = "Credit Memo",
|
||||
Description = $"Credit applied to {cm.Invoice?.InvoiceNumber}",
|
||||
Debit = 0,
|
||||
Credit = cm.AmountApplied,
|
||||
LinkController = "Invoices",
|
||||
LinkId = cm.InvoiceId
|
||||
});
|
||||
|
||||
// Refunds re-open AR (DEBIT — customer owes again after refund)
|
||||
var arRefunds = await _context.Refunds
|
||||
.Include(r => r.Invoice)
|
||||
.Where(r => r.RefundDate >= fromDate && r.RefundDate <= toDate && !r.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var r in arRefunds)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = r.RefundDate,
|
||||
Reference = r.Reference ?? $"REF-{r.Id}",
|
||||
Source = "Refund",
|
||||
Description = r.Reason,
|
||||
Debit = r.Amount,
|
||||
Credit = 0,
|
||||
LinkController = "Invoices",
|
||||
LinkId = r.InvoiceId
|
||||
});
|
||||
}
|
||||
|
||||
// ── 9. Accounts Payable ────────────────────────────────────────────────
|
||||
@@ -296,6 +375,102 @@ public class LedgerService : ILedgerService
|
||||
LinkController = "Bills",
|
||||
LinkId = bp.BillId
|
||||
});
|
||||
|
||||
// Vendor credit applications reduce AP (DEBIT — offset against what we owe)
|
||||
var apVendorCredits = await _context.VendorCreditApplications
|
||||
.Include(vca => vca.VendorCredit)
|
||||
.Include(vca => vca.Bill)
|
||||
.Where(vca => vca.VendorCredit.APAccountId == accountId
|
||||
&& vca.AppliedDate >= fromDate && vca.AppliedDate <= toDate)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var vca in apVendorCredits)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = vca.AppliedDate,
|
||||
Reference = vca.VendorCredit?.CreditNumber ?? $"VC-{vca.VendorCreditId}",
|
||||
Source = "Vendor Credit",
|
||||
Description = $"Credit applied to {vca.Bill?.BillNumber}",
|
||||
Debit = vca.Amount,
|
||||
Credit = 0,
|
||||
LinkController = "VendorCredits",
|
||||
LinkId = vca.VendorCreditId
|
||||
});
|
||||
}
|
||||
|
||||
// ── 11. Gift Certificate Liability (account 2500) ─────────────────────
|
||||
// CR when GC is issued; DR when redeemed or voided with remaining balance.
|
||||
if (account.AccountNumber == "2500")
|
||||
{
|
||||
var gcIssued = await _context.GiftCertificates
|
||||
.Where(gc => !gc.IsDeleted && gc.IssueDate >= fromDate && gc.IssueDate <= toDate)
|
||||
.ToListAsync();
|
||||
foreach (var gc in gcIssued)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = gc.IssueDate, Reference = gc.CertificateCode,
|
||||
Source = "Gift Certificate", Description = "GC issued",
|
||||
Debit = 0, Credit = gc.OriginalAmount,
|
||||
LinkController = "GiftCertificates", LinkId = gc.Id
|
||||
});
|
||||
|
||||
var gcRedemptions = await _context.GiftCertificateRedemptions
|
||||
.Include(r => r.GiftCertificate)
|
||||
.Where(r => !r.IsDeleted && r.RedeemedDate >= fromDate && r.RedeemedDate <= toDate)
|
||||
.ToListAsync();
|
||||
foreach (var r in gcRedemptions)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = r.RedeemedDate, Reference = r.GiftCertificate?.CertificateCode ?? $"GC-{r.GiftCertificateId}",
|
||||
Source = "GC Redemption", Description = "GC applied to invoice",
|
||||
Debit = r.AmountRedeemed, Credit = 0,
|
||||
LinkController = "GiftCertificates", LinkId = r.GiftCertificateId
|
||||
});
|
||||
|
||||
var gcVoided = await _context.GiftCertificates
|
||||
.Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
|
||||
&& gc.UpdatedAt >= fromDate && gc.UpdatedAt <= toDate
|
||||
&& gc.OriginalAmount > gc.RedeemedAmount)
|
||||
.ToListAsync();
|
||||
foreach (var gc in gcVoided)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = gc.UpdatedAt.GetValueOrDefault(), Reference = gc.CertificateCode,
|
||||
Source = "GC Voided", Description = "Breakage income",
|
||||
Debit = gc.OriginalAmount - gc.RedeemedAmount, Credit = 0,
|
||||
LinkController = "GiftCertificates", LinkId = gc.Id
|
||||
});
|
||||
}
|
||||
|
||||
// ── 12. Customer Deposits liability (account 2300) ────────────────────
|
||||
// CR when deposit is recorded; DR when deposit is applied to an invoice.
|
||||
if (account.AccountNumber == "2300")
|
||||
{
|
||||
var depositsRecorded = await _context.Deposits
|
||||
.Where(d => !d.IsDeleted && d.ReceivedDate >= fromDate && d.ReceivedDate <= toDate)
|
||||
.ToListAsync();
|
||||
foreach (var d in depositsRecorded)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = d.ReceivedDate, Reference = d.ReceiptNumber,
|
||||
Source = "Customer Deposit", Description = d.Notes ?? d.Reference,
|
||||
Debit = 0, Credit = d.Amount,
|
||||
LinkController = "Jobs", LinkId = d.JobId
|
||||
});
|
||||
|
||||
var depositsApplied = await _context.Deposits
|
||||
.Include(d => d.AppliedToInvoice)
|
||||
.Where(d => !d.IsDeleted && d.AppliedToInvoiceId != null
|
||||
&& d.AppliedDate >= fromDate && d.AppliedDate <= toDate)
|
||||
.ToListAsync();
|
||||
foreach (var d in depositsApplied)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = d.AppliedDate!.Value, Reference = d.AppliedToInvoice?.InvoiceNumber ?? d.ReceiptNumber,
|
||||
Source = "Deposit Applied", Description = $"Deposit {d.ReceiptNumber} applied to invoice",
|
||||
Debit = d.Amount, Credit = 0,
|
||||
LinkController = "Invoices", LinkId = d.AppliedToInvoiceId
|
||||
});
|
||||
}
|
||||
|
||||
// ── 10. Journal Entry lines touching this account ──────────────────
|
||||
@@ -382,6 +557,16 @@ public class LedgerService : ILedgerService
|
||||
.Where(p => p.DepositAccountId == accountId && p.PaymentDate < beforeDate)
|
||||
.SumAsync(p => (decimal?)p.Amount) ?? 0;
|
||||
|
||||
// Customer deposits recorded to this account (DEBIT — cash received at deposit time)
|
||||
debits += await _context.Deposits
|
||||
.Where(d => !d.IsDeleted && d.DepositAccountId == accountId && d.ReceivedDate < beforeDate)
|
||||
.SumAsync(d => (decimal?)d.Amount) ?? 0;
|
||||
|
||||
// Refunds paid FROM this account (CREDIT — cash leaves)
|
||||
credits += await _context.Refunds
|
||||
.Where(r => !r.IsDeleted && r.DepositAccountId == accountId && r.RefundDate < beforeDate)
|
||||
.SumAsync(r => (decimal?)r.Amount) ?? 0;
|
||||
|
||||
// 2. Direct expenses paid FROM this account (CREDIT)
|
||||
credits += await _context.Expenses
|
||||
.Where(e => e.PaymentAccountId == accountId && e.Date < beforeDate)
|
||||
@@ -434,6 +619,14 @@ public class LedgerService : ILedgerService
|
||||
credits += await _context.Payments
|
||||
.Where(p => p.PaymentDate < beforeDate)
|
||||
.SumAsync(p => (decimal?)p.Amount) ?? 0;
|
||||
|
||||
credits += await _context.CreditMemoApplications
|
||||
.Where(a => a.AppliedDate < beforeDate && a.Invoice.Status != InvoiceStatus.Voided)
|
||||
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0;
|
||||
|
||||
debits += await _context.Refunds
|
||||
.Where(r => !r.IsDeleted && r.RefundDate < beforeDate)
|
||||
.SumAsync(r => (decimal?)r.Amount) ?? 0;
|
||||
}
|
||||
|
||||
// 9. Accounts Payable
|
||||
@@ -449,6 +642,36 @@ public class LedgerService : ILedgerService
|
||||
debits += await _context.BillPayments
|
||||
.Where(bp => bp.Bill.APAccountId == accountId && bp.PaymentDate < beforeDate)
|
||||
.SumAsync(bp => (decimal?)bp.Amount) ?? 0;
|
||||
|
||||
debits += await _context.VendorCreditApplications
|
||||
.Where(vca => vca.VendorCredit.APAccountId == accountId && vca.AppliedDate < beforeDate)
|
||||
.SumAsync(vca => (decimal?)vca.Amount) ?? 0;
|
||||
}
|
||||
|
||||
// 11. GC Liability (account 2500)
|
||||
if (account.AccountNumber == "2500")
|
||||
{
|
||||
credits += await _context.GiftCertificates
|
||||
.Where(gc => !gc.IsDeleted && gc.IssueDate < beforeDate)
|
||||
.SumAsync(gc => (decimal?)gc.OriginalAmount) ?? 0;
|
||||
debits += await _context.GiftCertificateRedemptions
|
||||
.Where(r => !r.IsDeleted && r.RedeemedDate < beforeDate)
|
||||
.SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0;
|
||||
debits += await _context.GiftCertificates
|
||||
.Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
|
||||
&& gc.UpdatedAt < beforeDate && gc.OriginalAmount > gc.RedeemedAmount)
|
||||
.SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0;
|
||||
}
|
||||
|
||||
// 12. Customer Deposits liability (account 2300)
|
||||
if (account.AccountNumber == "2300")
|
||||
{
|
||||
credits += await _context.Deposits
|
||||
.Where(d => !d.IsDeleted && d.ReceivedDate < beforeDate)
|
||||
.SumAsync(d => (decimal?)d.Amount) ?? 0;
|
||||
debits += await _context.Deposits
|
||||
.Where(d => !d.IsDeleted && d.AppliedToInvoiceId != null && d.AppliedDate < beforeDate)
|
||||
.SumAsync(d => (decimal?)d.Amount) ?? 0;
|
||||
}
|
||||
|
||||
// 10. Posted journal entry lines touching this account (prior to period)
|
||||
|
||||
Reference in New Issue
Block a user