diff --git a/src/PowderCoating.Web/Controllers/InvoicesController.cs b/src/PowderCoating.Web/Controllers/InvoicesController.cs index 39ed6af..3abf803 100644 --- a/src/PowderCoating.Web/Controllers/InvoicesController.cs +++ b/src/PowderCoating.Web/Controllers/InvoicesController.cs @@ -1480,6 +1480,7 @@ public class InvoicesController : Controller } var currentUser = await _userManager.GetUserAsync(User); + var totalCreditCreated = 0m; // populated inside transaction, used in success message await _unitOfWork.ExecuteInTransactionAsync(async () => { @@ -1522,6 +1523,52 @@ public class InvoicesController : Controller await _accountBalanceService.CreditAsync(custDepositsAcctId, totalDepositReleased); } + // Convert non-deposit payments (cash, card, check, online) to customer credits so + // the money isn't lost when the invoice is voided. Each payment becomes a CRED- + // Deposit record linked to the same job; it will auto-apply when the replacement + // invoice is created, exactly like a normal deposit. + var nonDepositPayments = invoice.Payments + .Where(p => !p.IsDeleted && !(p.Reference ?? "").StartsWith("Deposit ")) + .ToList(); + if (nonDepositPayments.Any()) + { + var credPrefix = $"CRED-{DateTime.UtcNow:yy}{DateTime.UtcNow.Month:D2}-"; + var existingNums = (await _unitOfWork.Deposits.FindAsync( + d => d.CompanyId == invoice.CompanyId && d.ReceiptNumber.StartsWith(credPrefix), + ignoreQueryFilters: true)) + .Select(d => d.ReceiptNumber).ToList(); + var maxNum = 0; + foreach (var rn in existingNums) + { + var suffix = rn.Length >= credPrefix.Length + 4 ? rn[credPrefix.Length..] : ""; + if (int.TryParse(suffix, out int parsed) && parsed > maxNum) maxNum = parsed; + } + + var creditCustDepositsAcctId = await GetCustomerDepositsAccountIdAsync(invoice.CompanyId); + foreach (var payment in nonDepositPayments) + { + maxNum++; + await _unitOfWork.Deposits.AddAsync(new Core.Entities.Deposit + { + CompanyId = invoice.CompanyId, + CustomerId = invoice.CustomerId, + JobId = invoice.JobId, + Amount = payment.Amount, + PaymentMethod = payment.PaymentMethod, + ReceivedDate = payment.PaymentDate, + Reference = payment.Reference, + Notes = $"Credit from voided invoice {invoice.InvoiceNumber}" + + (string.IsNullOrWhiteSpace(payment.Notes) ? "." : $". Original: {payment.Notes}"), + ReceiptNumber = $"{credPrefix}{maxNum:D4}", + CreatedAt = DateTime.UtcNow + }); + totalCreditCreated += payment.Amount; + } + + // CR CustomerDeposits to create the liability matching the cash already in Checking + await _accountBalanceService.CreditAsync(creditCustDepositsAcctId, totalCreditCreated); + } + // Void any gift certificates that were generated from this invoice. // Capture each GC's remaining balance BEFORE voiding so the GL entries below can use it. var gcLiabilityAcctId = await GetGcLiabilityAccountIdAsync(invoice.CompanyId); @@ -1573,7 +1620,10 @@ public class InvoicesController : Controller }); // end ExecuteInTransactionAsync - TempData["Success"] = $"Invoice {invoice.InvoiceNumber} has been voided."; + var creditMsg = totalCreditCreated > 0 + ? $" {totalCreditCreated:C} converted to customer credit and will auto-apply to the next invoice." + : ""; + TempData["Success"] = $"Invoice {invoice.InvoiceNumber} has been voided.{creditMsg}"; return RedirectToAction(nameof(Details), new { id }); } catch (Exception ex) diff --git a/src/PowderCoating.Web/Views/Invoices/Details.cshtml b/src/PowderCoating.Web/Views/Invoices/Details.cshtml index 3079024..df65817 100644 --- a/src/PowderCoating.Web/Views/Invoices/Details.cshtml +++ b/src/PowderCoating.Web/Views/Invoices/Details.cshtml @@ -28,6 +28,9 @@ var onlinePaymentsEnabled = ViewBag.OnlinePaymentsEnabled == true; var showOnlinePaymentCard = !isDraft && !isVoided && Model.BalanceDue > 0 && onlinePaymentsEnabled; var guidedActivationCallout = ViewBag.GuidedActivationCallout as PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationCalloutViewModel; + var nonDepositPaymentTotal = Model.Payments + .Where(p => !(p.Reference ?? "").StartsWith("Deposit ")) + .Sum(p => p.Amount); }