Add Credit Memos standalone management module

CreditMemosController with Index, Details, Create, Apply, and Void actions.
All business logic (atomic apply transaction, RemainingBalance cap,
customer.CreditBalance adjustment, auto-Paid invoice when BalanceDue hits zero)
mirrors the invoice-centric IssueCreditMemo/ApplyCredit/VoidCreditMemo actions in
InvoicesController but redirects back to the credit memo rather than an invoice.

Views: Index (stats bar, status+search filter, table), Details (two-col layout
with application history table and Bootstrap Apply/Void confirm modals),
Create (customer dropdown, amount, reason, notes, optional expiry).

Apply modal populates amount automatically from min(remaining credit,
invoice balance due) via credit-memo.js data-attribute wiring (no inline scripts).

Nav: Credit Memos added to Billing & Payments section in _Layout.

Build: 0 errors. Unit tests: 200/200.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:48:35 -04:00
parent d94612cc9c
commit a255893ada
6 changed files with 954 additions and 0 deletions
@@ -0,0 +1,30 @@
(function () {
'use strict';
document.addEventListener('DOMContentLoaded', function () {
var modal = document.getElementById('applyModal');
if (!modal) return;
var remainingBalance = parseFloat(modal.dataset.remainingBalance || '0');
var invoiceSelect = document.getElementById('applyInvoiceId');
var amountInput = document.getElementById('applyAmount');
var maxHint = document.getElementById('applyMaxHint');
if (!invoiceSelect) return;
invoiceSelect.addEventListener('change', function () {
var selected = this.options[this.selectedIndex];
if (!selected || !selected.value) {
amountInput.value = '';
if (maxHint) maxHint.textContent = '';
return;
}
var invoiceBalance = parseFloat(selected.dataset.balance || '0');
var max = Math.min(remainingBalance, invoiceBalance);
amountInput.value = max.toFixed(2);
amountInput.max = max.toFixed(2);
if (maxHint) maxHint.textContent = 'Max applicable: $' + max.toFixed(2);
});
});
})();