Track store credit as a Customer Credits liability (GL)
Audit finding 9b: store-credit refunds and credit memos posted nothing to the GL on issue (only a CreditMemo + Customer.CreditBalance), so outstanding store credit was invisible on the balance sheet and the contra-revenue was recognized only on apply. Introduces a 2350 "Customer Credits" liability so the credit is on the books from issue to apply. Model (chosen): lifecycle-equivalent to before, plus the liability is tracked. - Issue (credit memos, goodwill, and store-credit refunds): DR Sales Discounts (4950) / CR Customer Credits (2350). - Apply: DR Customer Credits / CR AR (was DR Sales Discounts / CR AR). - Void unapplied remainder: DR Customer Credits / CR Sales Discounts. Posting updated in all 8 sites: CreditMemosController Create/Apply/Void and InvoicesController IssueCreditMemo/IssueRefund(store credit)/ApplyCredit/ VoidCreditMemo/CancelRefund. New 2350 account (seed + self-heal). Reporting moved in lockstep so the books still balance: the 4950 contra-revenue shifts from applied -> issued (active memos in full + applied portion of voided), the 2350 liability = unapplied balance on active memos, AR still credited by applications. Updated in FinancialReportService (balance sheet retained earnings, trial balance, P&L) and LedgerService (per-account + prior-balance 2350 section). Verified the balance-sheet identity for active and voided memos by hand; new ledger test covers the 2350 lifecycle. Build clean; 284 unit tests pass. Note: pre-existing quirks left untouched (out of 9b scope) — account 2300 is seeded as "Payroll Liabilities" but resolved as Customer Deposits in code, and LedgerService doesn't recompute 4950 so RecalculateBalances understates it; both predate this change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -94,10 +94,16 @@ public class FinancialReportService : IFinancialReportService
|
||||
.Where(i => i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
|
||||
&& i.DiscountAmount > 0 && i.InvoiceDate >= from && i.InvoiceDate <= toEnd)
|
||||
.SumAsync(i => (decimal?)i.DiscountAmount) ?? 0m;
|
||||
var periodCredits = await _context.CreditMemoApplications
|
||||
.Where(a => a.AppliedDate >= from && a.AppliedDate <= toEnd
|
||||
&& a.Invoice.Status != InvoiceStatus.Voided)
|
||||
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;
|
||||
// Credit-memo contra-revenue is recognized at issue (DR Sales Discounts). Net for the period =
|
||||
// memos issued in the period minus the unapplied remainder of memos voided in the period.
|
||||
var periodCmIssued = await _context.CreditMemos
|
||||
.Where(m => m.CompanyId == companyId && m.IssueDate >= from && m.IssueDate <= toEnd)
|
||||
.SumAsync(m => (decimal?)m.Amount) ?? 0m;
|
||||
var periodCmVoided = await _context.CreditMemos
|
||||
.Where(m => m.CompanyId == companyId && m.Status == CreditMemoStatus.Voided
|
||||
&& m.UpdatedAt >= from && m.UpdatedAt <= toEnd)
|
||||
.SumAsync(m => (decimal?)(m.Amount - m.AmountApplied)) ?? 0m;
|
||||
var periodCredits = periodCmIssued - periodCmVoided;
|
||||
var totalDeductions = periodDiscounts + periodCredits;
|
||||
if (totalDeductions > 0)
|
||||
revenueLines.Add(new FinancialReportLine
|
||||
@@ -296,9 +302,25 @@ public class FinancialReportService : IFinancialReportService
|
||||
&& p.Invoice.Status != InvoiceStatus.WrittenOff)
|
||||
.SumAsync(p => (decimal?)p.Amount) ?? 0;
|
||||
// Credit memo applications reduce open AR (CR AR when a credit is applied to an invoice).
|
||||
arCredits += await _context.CreditMemoApplications
|
||||
var cmAppliedBs = await _context.CreditMemoApplications
|
||||
.Where(a => a.AppliedDate <= asOfEnd && a.Invoice.Status != InvoiceStatus.Voided)
|
||||
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0;
|
||||
arCredits += cmAppliedBs;
|
||||
|
||||
// Customer Credits (2350): a credit memo books DR Sales Discounts / CR Customer Credits on issue,
|
||||
// then DR Customer Credits / CR AR on apply. Contra-revenue (retained earnings) = issued amount
|
||||
// (active in full + applied portion of voided); the 2350 liability = unapplied balance on active memos.
|
||||
var cmIssuedNonVoidedBs = await _context.CreditMemos
|
||||
.Where(m => m.CompanyId == companyId && m.Status != CreditMemoStatus.Voided && m.IssueDate <= asOfEnd)
|
||||
.SumAsync(m => (decimal?)m.Amount) ?? 0m;
|
||||
var cmAppliedNonVoidedBs = await _context.CreditMemoApplications
|
||||
.Where(a => a.AppliedDate <= asOfEnd && a.Invoice.Status != InvoiceStatus.Voided
|
||||
&& a.CreditMemo.Status != CreditMemoStatus.Voided)
|
||||
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;
|
||||
var cmContraRevenueBs = cmIssuedNonVoidedBs + (cmAppliedBs - cmAppliedNonVoidedBs);
|
||||
var customerCreditsAcctIdBs = await _context.Accounts
|
||||
.Where(a => a.CompanyId == companyId && a.AccountNumber == "2350" && a.IsActive && !a.IsDeleted)
|
||||
.Select(a => (int?)a.Id).FirstOrDefaultAsync();
|
||||
// Cash refunds reverse the sale: revenue portion reduces retained earnings (Sales Returns),
|
||||
// tax portion relieves Sales Tax Payable, cash leaves the bank (refundsByAcctBs). AR is untouched.
|
||||
// Store-credit refunds post via CreditMemo, not the GL, so are excluded.
|
||||
@@ -373,11 +395,9 @@ public class FinancialReportService : IFinancialReportService
|
||||
.Where(i => i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
|
||||
&& i.DiscountAmount > 0 && i.InvoiceDate <= asOfEnd)
|
||||
.SumAsync(i => (decimal?)i.DiscountAmount) ?? 0m);
|
||||
// Credit memos applied to invoices reduce net revenue (contra-revenue, same as discounts).
|
||||
var lifetimeCreditMemos = isCash ? 0m
|
||||
: (await _context.CreditMemoApplications
|
||||
.Where(a => a.AppliedDate <= asOfEnd && a.Invoice.Status != InvoiceStatus.Voided)
|
||||
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0m);
|
||||
// Credit memos are contra-revenue recognized at issue (DR Sales Discounts). Net revenue is
|
||||
// reduced by the issued amount (active memos in full + applied portion of voided memos).
|
||||
var lifetimeCreditMemos = isCash ? 0m : cmContraRevenueBs;
|
||||
var lifetimeDirectExp = await _context.Expenses
|
||||
.Where(e => e.Date <= asOfEnd)
|
||||
.SumAsync(e => (decimal?)e.Amount) ?? 0;
|
||||
@@ -470,6 +490,11 @@ public class FinancialReportService : IFinancialReportService
|
||||
credits += gcLiabilityCreditsBs; // GC issued → CR liability
|
||||
debits += gcLiabilityDebitsBs; // redeemed/voided → DR liability
|
||||
}
|
||||
if (customerCreditsAcctIdBs.HasValue && a.Id == customerCreditsAcctIdBs.Value)
|
||||
{
|
||||
credits += cmIssuedNonVoidedBs; // credit memos issued → CR liability
|
||||
debits += cmAppliedNonVoidedBs; // applied → DR liability
|
||||
}
|
||||
if (custDepositsAcctIdBs.HasValue && a.Id == custDepositsAcctIdBs.Value)
|
||||
{
|
||||
credits += custDepositsCreditsBs; // deposits taken → CR liability
|
||||
@@ -988,6 +1013,23 @@ public class FinancialReportService : IFinancialReportService
|
||||
&& a.Invoice.Status != InvoiceStatus.Voided)
|
||||
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;
|
||||
|
||||
// Customer Credits (2350) model: a credit memo books DR Sales Discounts / CR Customer Credits on
|
||||
// issue, then DR Customer Credits / CR AR on apply. So the 4950 contra-revenue is the *issued*
|
||||
// amount (active memos in full + the applied portion of voided memos), and the 2350 liability is
|
||||
// the unapplied balance on active memos. AR is still credited by applications (cmApplied).
|
||||
var cmIssuedNonVoided = await _context.CreditMemos
|
||||
.Where(m => m.CompanyId == companyId && m.Status != CreditMemoStatus.Voided && m.IssueDate <= asOfEnd)
|
||||
.SumAsync(m => (decimal?)m.Amount) ?? 0m;
|
||||
var cmAppliedNonVoided = await _context.CreditMemoApplications
|
||||
.Where(a => a.AppliedDate <= asOfEnd
|
||||
&& a.Invoice.Status != InvoiceStatus.Voided
|
||||
&& a.CreditMemo.Status != CreditMemoStatus.Voided)
|
||||
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;
|
||||
var cmContraRevenue = cmIssuedNonVoided + (cmApplied - cmAppliedNonVoided); // DR 4950
|
||||
var customerCreditsAcctId = await _context.Accounts
|
||||
.Where(a => a.CompanyId == companyId && a.AccountNumber == "2350" && a.IsActive && !a.IsDeleted)
|
||||
.Select(a => (int?)a.Id).FirstOrDefaultAsync();
|
||||
|
||||
var discountsByAcct = new Dictionary<int, decimal>();
|
||||
if (discountAcctId.HasValue)
|
||||
{
|
||||
@@ -997,8 +1039,8 @@ public class FinancialReportService : IFinancialReportService
|
||||
&& i.Status != InvoiceStatus.Voided
|
||||
&& i.InvoiceDate <= asOfEnd)
|
||||
.SumAsync(i => (decimal?)i.DiscountAmount) ?? 0m;
|
||||
if (totalDiscounts + cmApplied > 0)
|
||||
discountsByAcct[discountAcctId.Value] = totalDiscounts + cmApplied;
|
||||
if (totalDiscounts + cmContraRevenue > 0)
|
||||
discountsByAcct[discountAcctId.Value] = totalDiscounts + cmContraRevenue;
|
||||
}
|
||||
|
||||
// JE lines: posted entries debit/credit all account types
|
||||
@@ -1149,6 +1191,11 @@ public class FinancialReportService : IFinancialReportService
|
||||
credits += custDepositsCredits; // deposits taken → CR liability
|
||||
debits += custDepositsDebits; // deposits applied → DR liability
|
||||
}
|
||||
if (customerCreditsAcctId.HasValue && a.Id == customerCreditsAcctId.Value)
|
||||
{
|
||||
credits += cmIssuedNonVoided; // credit memos issued → CR liability
|
||||
debits += cmAppliedNonVoided; // applied → DR liability
|
||||
}
|
||||
}
|
||||
|
||||
// Manual JEs apply to all account types (including AR/AP for unusual adjustments)
|
||||
|
||||
@@ -495,6 +495,39 @@ public class LedgerService : ILedgerService
|
||||
});
|
||||
}
|
||||
|
||||
// ── 12b. Customer Credits liability (account 2350) ────────────────────
|
||||
// CR when a credit memo (incl. store-credit refund) is issued; DR when applied to an invoice.
|
||||
// Voided memos are excluded (their issue/void net to zero).
|
||||
if (account.AccountNumber == "2350")
|
||||
{
|
||||
var memosIssued = await _context.CreditMemos
|
||||
.Where(m => m.Status != CreditMemoStatus.Voided
|
||||
&& m.IssueDate >= fromDate && m.IssueDate <= toDate)
|
||||
.ToListAsync();
|
||||
foreach (var m in memosIssued)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = m.IssueDate, Reference = m.MemoNumber,
|
||||
Source = "Credit Memo", Description = "Store credit issued",
|
||||
Debit = 0, Credit = m.Amount,
|
||||
LinkController = "CreditMemos", LinkId = m.Id
|
||||
});
|
||||
|
||||
var memosApplied = await _context.CreditMemoApplications
|
||||
.Include(a => a.CreditMemo).Include(a => a.Invoice)
|
||||
.Where(a => a.CreditMemo.Status != CreditMemoStatus.Voided
|
||||
&& a.AppliedDate >= fromDate && a.AppliedDate <= toDate)
|
||||
.ToListAsync();
|
||||
foreach (var a in memosApplied)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = a.AppliedDate, Reference = a.CreditMemo?.MemoNumber ?? $"CM-{a.CreditMemoId}",
|
||||
Source = "Credit Applied", Description = $"Applied to {a.Invoice?.InvoiceNumber}",
|
||||
Debit = a.AmountApplied, Credit = 0,
|
||||
LinkController = "Invoices", LinkId = a.InvoiceId
|
||||
});
|
||||
}
|
||||
|
||||
// ── 10. Journal Entry lines touching this account ──────────────────
|
||||
var jeLines = await _context.JournalEntryLines
|
||||
.Include(l => l.JournalEntry)
|
||||
@@ -716,6 +749,17 @@ public class LedgerService : ILedgerService
|
||||
.SumAsync(d => (decimal?)d.Amount) ?? 0;
|
||||
}
|
||||
|
||||
// 12b. Customer Credits liability (account 2350)
|
||||
if (account.AccountNumber == "2350")
|
||||
{
|
||||
credits += await _context.CreditMemos
|
||||
.Where(m => m.Status != CreditMemoStatus.Voided && m.IssueDate < beforeDate)
|
||||
.SumAsync(m => (decimal?)m.Amount) ?? 0;
|
||||
debits += await _context.CreditMemoApplications
|
||||
.Where(a => a.CreditMemo.Status != CreditMemoStatus.Voided && a.AppliedDate < beforeDate)
|
||||
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0;
|
||||
}
|
||||
|
||||
// 10. Posted journal entry lines touching this account (prior to period)
|
||||
debits += await _context.JournalEntryLines
|
||||
.Where(l => l.AccountId == accountId
|
||||
|
||||
@@ -61,6 +61,7 @@ public partial class SeedDataService
|
||||
new Account { AccountNumber = "2100", Name = "Credit Card Payable", AccountType = AccountType.Liability, AccountSubType = AccountSubType.CreditCard, IsSystem = false, IsActive = true, Description = "Business credit card balance", CompanyId = company.Id, CreatedAt = now },
|
||||
new Account { AccountNumber = "2200", Name = "Sales Tax Payable", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = false, IsActive = true, Description = "Sales tax collected and owed to government", CompanyId = company.Id, CreatedAt = now },
|
||||
new Account { AccountNumber = "2300", Name = "Payroll Liabilities", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = false, IsActive = true, Description = "Payroll taxes and withholdings owed", CompanyId = company.Id, CreatedAt = now },
|
||||
new Account { AccountNumber = "2350", Name = "Customer Credits", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = true, IsActive = true, Description = "Store credit owed to customers (credit memos not yet applied)", CompanyId = company.Id, CreatedAt = now },
|
||||
new Account { AccountNumber = "2900", Name = "Business Loan", AccountType = AccountType.Liability, AccountSubType = AccountSubType.LongTermLiability, IsSystem = false, IsActive = true, Description = "Long-term equipment or business loan", CompanyId = company.Id, CreatedAt = now },
|
||||
|
||||
// ── EQUITY ────────────────────────────────────────────────────────
|
||||
@@ -166,6 +167,30 @@ public partial class SeedDataService
|
||||
added++;
|
||||
}
|
||||
|
||||
// 2350 Customer Credits — liability for store credit owed to customers. Credited when a credit
|
||||
// memo (incl. store-credit refunds) is issued; debited when applied to an invoice.
|
||||
var has2350 = await _context.Set<Account>()
|
||||
.IgnoreQueryFilters()
|
||||
.AnyAsync(a => a.CompanyId == company.Id && a.AccountNumber == "2350" && !a.IsDeleted);
|
||||
|
||||
if (!has2350)
|
||||
{
|
||||
_context.Set<Account>().Add(new Account
|
||||
{
|
||||
AccountNumber = "2350",
|
||||
Name = "Customer Credits",
|
||||
AccountType = AccountType.Liability,
|
||||
AccountSubType = AccountSubType.OtherCurrentLiability,
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
Description = "Store credit owed to customers (credit memos not yet applied)",
|
||||
CompanyId = company.Id,
|
||||
CreatedAt = now
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
added++;
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,13 @@ public class CreditMemosController : Controller
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// GL: the credit is a liability owed to the customer — DR Sales Discounts (contra-revenue)
|
||||
// / CR Customer Credits (2350). Relieved when the memo is applied to an invoice.
|
||||
var discountAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.AccountNumber == "4950" && a.IsActive);
|
||||
var customerCreditsAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.AccountNumber == "2350" && a.IsActive);
|
||||
await _accountBalanceService.DebitAsync(discountAcct?.Id, vm.Amount);
|
||||
await _accountBalanceService.CreditAsync(customerCreditsAcct?.Id, vm.Amount);
|
||||
|
||||
TempData["Success"] = $"Credit memo {memoNumber} for {vm.Amount:C} issued to {DisplayName(customer)}.";
|
||||
return RedirectToAction(nameof(Details), new { id = memo.Id });
|
||||
}
|
||||
@@ -252,18 +259,14 @@ public class CreditMemosController : Controller
|
||||
await _unitOfWork.Invoices.UpdateAsync(invoice);
|
||||
}
|
||||
|
||||
// GL: DR 4950 Sales Discounts (contra-revenue) / CR AR.
|
||||
// The dynamic report computation attributes credit memo applications to both
|
||||
// accounts already; this call keeps Account.CurrentBalance in sync for
|
||||
// RecalculateAllAsync and any tools that read it directly.
|
||||
// GL: applying the credit relieves the liability — DR Customer Credits (2350) / CR AR.
|
||||
// The contra-revenue (Sales Discounts) was recognized when the credit was issued.
|
||||
// Keeps Account.CurrentBalance in sync for RecalculateAllAsync and direct readers.
|
||||
var arAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.AccountSubType == AccountSubType.AccountsReceivable && a.IsActive);
|
||||
var discountAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.AccountNumber == "4950" && a.IsActive)
|
||||
?? await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.AccountType == AccountType.Revenue && a.IsActive
|
||||
&& a.Name.ToLower().Contains("discount"));
|
||||
await _accountBalanceService.DebitAsync(discountAcct?.Id, applyAmount);
|
||||
var customerCreditsAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.AccountNumber == "2350" && a.IsActive);
|
||||
await _accountBalanceService.DebitAsync(customerCreditsAcct?.Id, applyAmount);
|
||||
await _accountBalanceService.CreditAsync(arAcct?.Id, applyAmount);
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
@@ -308,6 +311,15 @@ public class CreditMemosController : Controller
|
||||
await _unitOfWork.Customers.UpdateAsync(memo.Customer);
|
||||
}
|
||||
|
||||
// GL: reverse the unapplied issuance — DR Customer Credits (2350) / CR Sales Discounts.
|
||||
if (remaining > 0)
|
||||
{
|
||||
var ccAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.AccountNumber == "2350" && a.IsActive);
|
||||
var sdAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.AccountNumber == "4950" && a.IsActive);
|
||||
await _accountBalanceService.DebitAsync(ccAcct?.Id, remaining);
|
||||
await _accountBalanceService.CreditAsync(sdAcct?.Id, remaining);
|
||||
}
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
TempData["Success"] = "Credit memo voided. Unapplied balance reversed from customer credit.";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
|
||||
@@ -2494,6 +2494,14 @@ public class InvoicesController : Controller
|
||||
return acct?.Id;
|
||||
}
|
||||
|
||||
/// <summary>Returns the Customer Credits liability account (2350) for store credit / credit memos.</summary>
|
||||
private async Task<int?> GetCustomerCreditsAccountIdAsync(int companyId)
|
||||
{
|
||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.IsActive && a.AccountNumber == "2350");
|
||||
return acct?.Id;
|
||||
}
|
||||
|
||||
/// <summary>Returns the AR account ID for the given company (first active AccountsReceivable account).</summary>
|
||||
private async Task<int?> GetArAccountIdAsync(int companyId)
|
||||
{
|
||||
@@ -2672,6 +2680,14 @@ public class InvoicesController : Controller
|
||||
}
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// GL: store credit is a liability owed to the customer — DR Sales Discounts (contra-revenue)
|
||||
// / CR Customer Credits (2350). The liability is relieved when the credit memo is applied.
|
||||
var scDiscountAcctId = await GetSalesDiscountAccountIdAsync(companyId);
|
||||
var scCustomerCreditsAcctId = await GetCustomerCreditsAccountIdAsync(companyId);
|
||||
await _accountBalanceService.DebitAsync(scDiscountAcctId, dto.Amount);
|
||||
await _accountBalanceService.CreditAsync(scCustomerCreditsAcctId, dto.Amount);
|
||||
|
||||
TempData["Success"] = $"Refund of {dto.Amount:C} applied as store credit. Credit memo {memoNumber} created.";
|
||||
}
|
||||
else
|
||||
@@ -2740,12 +2756,14 @@ public class InvoicesController : Controller
|
||||
|
||||
if (refund.RefundMethod == PaymentMethod.StoreCredit)
|
||||
{
|
||||
// Cancel the linked CreditMemo and reverse the CreditBalance
|
||||
// Cancel the linked CreditMemo and reverse the unapplied store-credit remainder.
|
||||
decimal creditReversed = refund.Amount;
|
||||
if (refund.CreditMemoId.HasValue)
|
||||
{
|
||||
var memo = await _unitOfWork.CreditMemos.GetByIdAsync(refund.CreditMemoId.Value);
|
||||
if (memo != null && memo.Status == CreditMemoStatus.Active)
|
||||
{
|
||||
creditReversed = memo.Amount - memo.AmountApplied; // only the unapplied remainder
|
||||
memo.Status = CreditMemoStatus.Voided;
|
||||
memo.UpdatedAt = DateTime.UtcNow;
|
||||
await _unitOfWork.CreditMemos.UpdateAsync(memo);
|
||||
@@ -2754,9 +2772,18 @@ public class InvoicesController : Controller
|
||||
|
||||
if (customer != null)
|
||||
{
|
||||
customer.CreditBalance -= refund.Amount;
|
||||
customer.CreditBalance = Math.Max(0, customer.CreditBalance - creditReversed);
|
||||
await _unitOfWork.Customers.UpdateAsync(customer);
|
||||
}
|
||||
|
||||
// GL: reverse the unapplied store-credit issuance — DR Customer Credits / CR Sales Discounts.
|
||||
if (creditReversed > 0)
|
||||
{
|
||||
var ccAcctId = await GetCustomerCreditsAccountIdAsync(refund.Invoice.CompanyId);
|
||||
var sdAcctId = await GetSalesDiscountAccountIdAsync(refund.Invoice.CompanyId);
|
||||
await _accountBalanceService.DebitAsync(ccAcctId, creditReversed);
|
||||
await _accountBalanceService.CreditAsync(sdAcctId, creditReversed);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2831,6 +2858,14 @@ public class InvoicesController : Controller
|
||||
}
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// GL: the credit is a liability owed to the customer — DR Sales Discounts (contra-revenue)
|
||||
// / CR Customer Credits (2350). Relieved when the memo is applied to an invoice.
|
||||
var cmDiscountAcctId = await GetSalesDiscountAccountIdAsync(companyId);
|
||||
var cmCustomerCreditsAcctId = await GetCustomerCreditsAccountIdAsync(companyId);
|
||||
await _accountBalanceService.DebitAsync(cmDiscountAcctId, dto.Amount);
|
||||
await _accountBalanceService.CreditAsync(cmCustomerCreditsAcctId, dto.Amount);
|
||||
|
||||
TempData["Success"] = $"Credit memo {memoNumber} for {dto.Amount:C} issued to customer.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -2917,9 +2952,11 @@ public class InvoicesController : Controller
|
||||
}
|
||||
|
||||
// GL: DR Sales Discounts 4950 / CR AR — same as CreditMemosController.Apply.
|
||||
// GL: applying the credit relieves the liability — DR Customer Credits (2350) / CR AR.
|
||||
// (The contra-revenue was already recognized as Sales Discounts when the credit was issued.)
|
||||
var arAccountId = await GetArAccountIdAsync(invoice.CompanyId);
|
||||
var discountAcctId = await GetSalesDiscountAccountIdAsync(invoice.CompanyId);
|
||||
await _accountBalanceService.DebitAsync(discountAcctId, applyAmount);
|
||||
var customerCreditsAcctId = await GetCustomerCreditsAccountIdAsync(invoice.CompanyId);
|
||||
await _accountBalanceService.DebitAsync(customerCreditsAcctId, applyAmount);
|
||||
await _accountBalanceService.CreditAsync(arAccountId, applyAmount);
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
@@ -2962,6 +2999,15 @@ public class InvoicesController : Controller
|
||||
await _unitOfWork.Customers.UpdateAsync(memo.Customer);
|
||||
}
|
||||
|
||||
// GL: reverse the unapplied issuance — DR Customer Credits (2350) / CR Sales Discounts.
|
||||
if (remaining > 0)
|
||||
{
|
||||
var ccAcctId = await GetCustomerCreditsAccountIdAsync(memo.CompanyId);
|
||||
var sdAcctId = await GetSalesDiscountAccountIdAsync(memo.CompanyId);
|
||||
await _accountBalanceService.DebitAsync(ccAcctId, remaining);
|
||||
await _accountBalanceService.CreditAsync(sdAcctId, remaining);
|
||||
}
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
TempData["Success"] = "Credit memo voided.";
|
||||
return RedirectToAction(nameof(Details), new { id = invoiceId });
|
||||
|
||||
Reference in New Issue
Block a user