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:
@@ -10,6 +10,7 @@ using PowderCoating.Core.Enums;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Shared.Constants;
|
||||
using PowderCoating.Web.Helpers;
|
||||
using AccountSubTypeEnum = PowderCoating.Core.Enums.AccountSubType;
|
||||
|
||||
namespace PowderCoating.Web.Controllers;
|
||||
|
||||
@@ -31,6 +32,7 @@ public class GiftCertificatesController : Controller
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IPdfService _pdfService;
|
||||
private readonly ICompanyLogoService _logoService;
|
||||
private readonly IAccountBalanceService _accountBalanceService;
|
||||
|
||||
public GiftCertificatesController(
|
||||
IUnitOfWork unitOfWork,
|
||||
@@ -38,7 +40,8 @@ public class GiftCertificatesController : Controller
|
||||
ILogger<GiftCertificatesController> logger,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IPdfService pdfService,
|
||||
ICompanyLogoService logoService)
|
||||
ICompanyLogoService logoService,
|
||||
IAccountBalanceService accountBalanceService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
@@ -46,6 +49,7 @@ public class GiftCertificatesController : Controller
|
||||
_userManager = userManager;
|
||||
_pdfService = pdfService;
|
||||
_logoService = logoService;
|
||||
_accountBalanceService = accountBalanceService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -240,6 +244,26 @@ public class GiftCertificatesController : Controller
|
||||
await _unitOfWork.GiftCertificates.AddAsync(cert);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// GL: CR Gift Certificate Liability (2500) for the face value.
|
||||
// Debit side varies by reason:
|
||||
// Sold → DR Checking (received cash outside invoice flow)
|
||||
// Others → DR Sales Discounts 4950 (promotional/goodwill cost)
|
||||
var gcLiabilityAcctId = await GetGcLiabilityAccountIdAsync(companyId);
|
||||
await _accountBalanceService.CreditAsync(gcLiabilityAcctId, cert.OriginalAmount);
|
||||
if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
|
||||
{
|
||||
var checkingAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.IsActive && (a.AccountSubType == AccountSubTypeEnum.Checking
|
||||
|| a.AccountSubType == AccountSubTypeEnum.Cash));
|
||||
await _accountBalanceService.DebitAsync(checkingAcctId?.Id, cert.OriginalAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
var discountAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.IsActive && a.AccountNumber == "4950");
|
||||
await _accountBalanceService.DebitAsync(discountAcctId?.Id, cert.OriginalAmount);
|
||||
}
|
||||
|
||||
TempData["Success"] = $"Gift certificate {code} for {dto.Amount:C} created successfully.";
|
||||
return RedirectToAction(nameof(Details), new { id = cert.Id });
|
||||
}
|
||||
@@ -272,11 +296,24 @@ public class GiftCertificatesController : Controller
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
var remaining = cert.RemainingBalance;
|
||||
cert.Status = GiftCertificateStatus.Voided;
|
||||
cert.UpdatedAt = DateTime.UtcNow;
|
||||
await _unitOfWork.GiftCertificates.UpdateAsync(cert);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// GL: DR GC Liability / CR Other Income (breakage — the company keeps the unredeemed amount)
|
||||
if (remaining > 0)
|
||||
{
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
var companyId = currentUser?.CompanyId ?? 0;
|
||||
var gcLiabilityAcctId = await GetGcLiabilityAccountIdAsync(companyId);
|
||||
var otherIncomeAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.IsActive && a.AccountSubType == AccountSubTypeEnum.OtherIncome);
|
||||
await _accountBalanceService.DebitAsync(gcLiabilityAcctId, remaining);
|
||||
await _accountBalanceService.CreditAsync(otherIncomeAcctId?.Id, remaining);
|
||||
}
|
||||
|
||||
TempData["Success"] = $"Gift certificate {cert.CertificateCode} has been voided.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
@@ -395,6 +432,14 @@ public class GiftCertificatesController : Controller
|
||||
ViewBag.Customers = list;
|
||||
}
|
||||
|
||||
/// <summary>Returns the Gift Certificate Liability account ID (account 2500) for the company.</summary>
|
||||
private async Task<int?> GetGcLiabilityAccountIdAsync(int companyId)
|
||||
{
|
||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.IsActive && a.AccountNumber == "2500");
|
||||
return acct?.Id;
|
||||
}
|
||||
|
||||
private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company? company)
|
||||
{
|
||||
if (company == null) return (null, null);
|
||||
|
||||
Reference in New Issue
Block a user