Convert non-deposit payments to customer credits on invoice void
When voiding an invoice that has non-deposit payments (e.g. CC charges), those payments are now converted to CRED- Deposit records so the money trail is preserved and the credit auto-applies to the replacement invoice. Deposits that were applied to the voided invoice are also re-released so they can auto-apply again. Void confirmation dialog and success message both reflect the credit amount when applicable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user