Scope GL posting account lookups by CompanyId; cap sales-tax remittance (audit O3, O4)
O3: defense-in-depth on the write/posting path. Finding #7 scoped the report (read) path; this scopes every GL posting-path account lookup that determines where money lands, so a SuperAdmin acting in a company context can never post to another tenant's account: - InvoicesController: all account-resolver helpers (checking, customer deposits, sales returns, customer credits, AR, bad debt, sales tax, sales discount, GC liability) plus the bank-account and write-off expense dropdowns - CreditMemosController: Create/Apply/Void GL lookups (scoped via the in-scope customer/invoice/memo) - GiftCertificatesController: Create/BulkCreate/Void GL lookups + GC liability helper - BillsController: AP/expense account resolution that pre-fills APAccountId DepositsController and JournalEntriesController.SalesTaxPayment were already scoped. O4: SalesTaxPayment now rejects a remittance greater than the outstanding Sales Tax Payable balance (0.005 rounding tolerance), so a typo can no longer drive 2200 into an abnormal debit balance. Remaining pure read-path dropdown lookups (app-wide, lower risk) are documented in docs/ACCOUNTING_AUDIT.md as a separate follow-up. All audit findings O1-O4 are now resolved. Build clean; 291 unit tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+26
-21
@@ -80,29 +80,34 @@ migration applied to the dev database successfully.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Open findings (deferred — to do after O1/O2 per user)
|
### O3 — Write-path account lookups omitted explicit `CompanyId` — **RESOLVED**
|
||||||
|
- Added `CompanyId` to every **GL posting-path** account lookup that determines where money posts:
|
||||||
|
- `InvoicesController` — all account-resolver helpers (`GetCheckingAccountIdAsync`,
|
||||||
|
`GetCustomerDepositsAccountIdAsync`, `GetSalesReturnsAccountIdAsync`, `GetCustomerCreditsAccountIdAsync`,
|
||||||
|
`GetArAccountIdAsync`, `GetBadDebtAccountIdAsync`, `ResolveSalesTaxAccountIdAsync`,
|
||||||
|
`GetSalesDiscountAccountIdAsync`, `GetGcLiabilityAccountIdAsync`) plus the bank-account and write-off
|
||||||
|
expense dropdowns (scoped via `_tenantContext`).
|
||||||
|
- `CreditMemosController` — Create/Apply/Void GL lookups (scoped via the in-scope `customer`/`invoice`/`memo`).
|
||||||
|
- `GiftCertificatesController` — Create, BulkCreate, Void GL lookups + `GetGcLiabilityAccountIdAsync`.
|
||||||
|
- `BillsController` — AP/expense account resolution that pre-fills `APAccountId` (Create + CreateFromPO).
|
||||||
|
- `DepositsController` and `JournalEntriesController.SalesTaxPayment` already scoped correctly.
|
||||||
|
|
||||||
### O3 — Write-path account lookups omit explicit `CompanyId` (defense-in-depth gap) — **LOW-MEDIUM**
|
### O4 — Sales-tax remittance could over-remit (drive 2200 negative) — **RESOLVED**
|
||||||
- Finding #7 added explicit `CompanyId` predicates to the **read** path, but several **posting** lookups
|
- `JournalEntriesController.SalesTaxPayment` (POST) now rejects any amount exceeding `taxAcct.CurrentBalance`
|
||||||
still rely on the global filter alone, e.g. `CreditMemosController` (182-184, 268, 317-318) and
|
(0.005 rounding tolerance), so a typo can no longer push Sales Tax Payable into an abnormal debit balance.
|
||||||
`InvoicesController` refund/credit account lookups use `FirstOrDefaultAsync(a => a.AccountNumber == "2350" && a.IsActive)`
|
|
||||||
with no `CompanyId`. `JournalEntriesController.SalesTaxPayment` (246) does it correctly.
|
|
||||||
- **Impact:** low in practice (these run under a non-SuperAdmin company context where the global filter
|
|
||||||
applies), but it violates the repo's standing rule and would post to the wrong tenant's account if ever
|
|
||||||
executed in a SuperAdmin/multi-company context.
|
|
||||||
- **Fix direction:** add `a.CompanyId == companyId` to every account lookup on the posting path.
|
|
||||||
|
|
||||||
### O4 — Sales-tax remittance can over-remit (drive 2200 negative) — **LOW**
|
Verification of O3+O4: `dotnet build` clean; `dotnet test tests/PowderCoating.UnitTests` → **291 passed**.
|
||||||
- `SalesTaxPayment` validates amount > 0 but does **not** cap the payment at the outstanding 2200 balance.
|
|
||||||
- **Impact:** a typo can push Sales Tax Payable negative (a debit-balance liability), which is an
|
|
||||||
abnormal balance that then surfaces oddly on the balance sheet.
|
|
||||||
- **Fix direction:** show the current 2200 balance (already shown on the GET form) and reject/warn when
|
|
||||||
`amount` exceeds it.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Suggested priority order
|
## Remaining (non-O1–O4) — known lower-risk follow-up
|
||||||
1. **O1** (mislabeled/commingled liability — real balance-sheet correctness issue)
|
Pure **read-path dropdown/display** account lookups still rely on the global tenant filter (correct for
|
||||||
2. **O2** (recalc corruption + reconciliation report reliability)
|
non-SuperAdmin; only a SuperAdmin acting inside a company could see another tenant's accounts in a picker).
|
||||||
3. **O3** (defense-in-depth on write path)
|
These are app-wide, not specific to the accounting posting path, e.g. `VendorCreditsController`
|
||||||
4. **O4** (input guard)
|
PopulateDropdowns/Details, `BillsController` PopulateDropdowns and Index/edit account lists,
|
||||||
|
`ExpensesController` filter + categorization pickers. Tracked here so they aren't mistaken for a regression;
|
||||||
|
fold into a broader defense-in-depth pass if/when desired.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
All audit findings **O1–O4 are resolved** on `dev`. Original audit numbering #1–3/#5/#6/#8 remains
|
||||||
|
unrecoverable (see top). The accounting batch has **not** been merged to `master`/production yet.
|
||||||
|
|||||||
@@ -202,14 +202,14 @@ public class BillsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var apAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var apAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.AccountSubType == AccountSubType.AccountsPayable);
|
a => a.CompanyId == po.CompanyId && a.AccountSubType == AccountSubType.AccountsPayable);
|
||||||
|
|
||||||
// Vendor default expense account, fall back to first expense/COGS account
|
// Vendor default expense account, fall back to first expense/COGS account
|
||||||
int? defaultExpenseAccountId = po.Vendor?.DefaultExpenseAccountId;
|
int? defaultExpenseAccountId = po.Vendor?.DefaultExpenseAccountId;
|
||||||
if (!defaultExpenseAccountId.HasValue)
|
if (!defaultExpenseAccountId.HasValue)
|
||||||
{
|
{
|
||||||
var fallbackAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var fallbackAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && (a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods));
|
a => a.CompanyId == po.CompanyId && a.IsActive && (a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods));
|
||||||
defaultExpenseAccountId = fallbackAccount?.Id;
|
defaultExpenseAccountId = fallbackAccount?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,8 +272,9 @@ public class BillsController : Controller
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Pre-fill AP account
|
// Pre-fill AP account
|
||||||
|
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||||
var apAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var apAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.AccountSubType == AccountSubType.AccountsPayable);
|
a => a.CompanyId == companyId && a.AccountSubType == AccountSubType.AccountsPayable);
|
||||||
dto.APAccountId = apAccount?.Id ?? 0;
|
dto.APAccountId = apAccount?.Id ?? 0;
|
||||||
|
|
||||||
// Pre-fill default expense account for vendor
|
// Pre-fill default expense account for vendor
|
||||||
|
|||||||
@@ -179,8 +179,8 @@ public class CreditMemosController : Controller
|
|||||||
|
|
||||||
// GL: the credit is a liability owed to the customer — DR Sales Discounts (contra-revenue)
|
// 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.
|
// / 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 discountAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.CompanyId == customer.CompanyId && a.AccountNumber == "4950" && a.IsActive);
|
||||||
var customerCreditsAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.AccountNumber == "2350" && a.IsActive);
|
var customerCreditsAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.CompanyId == customer.CompanyId && a.AccountNumber == "2350" && a.IsActive);
|
||||||
await _accountBalanceService.DebitAsync(discountAcct?.Id, vm.Amount);
|
await _accountBalanceService.DebitAsync(discountAcct?.Id, vm.Amount);
|
||||||
await _accountBalanceService.CreditAsync(customerCreditsAcct?.Id, vm.Amount);
|
await _accountBalanceService.CreditAsync(customerCreditsAcct?.Id, vm.Amount);
|
||||||
|
|
||||||
@@ -263,9 +263,9 @@ public class CreditMemosController : Controller
|
|||||||
// The contra-revenue (Sales Discounts) was recognized when the credit was issued.
|
// The contra-revenue (Sales Discounts) was recognized when the credit was issued.
|
||||||
// Keeps Account.CurrentBalance in sync for RecalculateAllAsync and direct readers.
|
// Keeps Account.CurrentBalance in sync for RecalculateAllAsync and direct readers.
|
||||||
var arAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var arAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.AccountSubType == AccountSubType.AccountsReceivable && a.IsActive);
|
a => a.CompanyId == invoice.CompanyId && a.AccountSubType == AccountSubType.AccountsReceivable && a.IsActive);
|
||||||
var customerCreditsAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var customerCreditsAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.AccountNumber == "2350" && a.IsActive);
|
a => a.CompanyId == invoice.CompanyId && a.AccountNumber == "2350" && a.IsActive);
|
||||||
await _accountBalanceService.DebitAsync(customerCreditsAcct?.Id, applyAmount);
|
await _accountBalanceService.DebitAsync(customerCreditsAcct?.Id, applyAmount);
|
||||||
await _accountBalanceService.CreditAsync(arAcct?.Id, applyAmount);
|
await _accountBalanceService.CreditAsync(arAcct?.Id, applyAmount);
|
||||||
|
|
||||||
@@ -314,8 +314,8 @@ public class CreditMemosController : Controller
|
|||||||
// GL: reverse the unapplied issuance — DR Customer Credits (2350) / CR Sales Discounts.
|
// GL: reverse the unapplied issuance — DR Customer Credits (2350) / CR Sales Discounts.
|
||||||
if (remaining > 0)
|
if (remaining > 0)
|
||||||
{
|
{
|
||||||
var ccAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.AccountNumber == "2350" && a.IsActive);
|
var ccAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.CompanyId == memo.CompanyId && a.AccountNumber == "2350" && a.IsActive);
|
||||||
var sdAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.AccountNumber == "4950" && a.IsActive);
|
var sdAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.CompanyId == memo.CompanyId && a.AccountNumber == "4950" && a.IsActive);
|
||||||
await _accountBalanceService.DebitAsync(ccAcct?.Id, remaining);
|
await _accountBalanceService.DebitAsync(ccAcct?.Id, remaining);
|
||||||
await _accountBalanceService.CreditAsync(sdAcct?.Id, remaining);
|
await _accountBalanceService.CreditAsync(sdAcct?.Id, remaining);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,14 +254,14 @@ public class GiftCertificatesController : Controller
|
|||||||
if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
|
if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
|
||||||
{
|
{
|
||||||
var checkingAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var checkingAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && (a.AccountSubType == AccountSubTypeEnum.Checking
|
a => a.CompanyId == companyId && a.IsActive && (a.AccountSubType == AccountSubTypeEnum.Checking
|
||||||
|| a.AccountSubType == AccountSubTypeEnum.Cash));
|
|| a.AccountSubType == AccountSubTypeEnum.Cash));
|
||||||
await _accountBalanceService.DebitAsync(checkingAcctId?.Id, cert.OriginalAmount);
|
await _accountBalanceService.DebitAsync(checkingAcctId?.Id, cert.OriginalAmount);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var discountAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var discountAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && a.AccountNumber == "4950");
|
a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "4950");
|
||||||
await _accountBalanceService.DebitAsync(discountAcctId?.Id, cert.OriginalAmount);
|
await _accountBalanceService.DebitAsync(discountAcctId?.Id, cert.OriginalAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ public class GiftCertificatesController : Controller
|
|||||||
var companyId = currentUser?.CompanyId ?? 0;
|
var companyId = currentUser?.CompanyId ?? 0;
|
||||||
var gcLiabilityAcctId = await GetGcLiabilityAccountIdAsync(companyId);
|
var gcLiabilityAcctId = await GetGcLiabilityAccountIdAsync(companyId);
|
||||||
var otherIncomeAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var otherIncomeAcctId = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && a.AccountSubType == AccountSubTypeEnum.OtherIncome);
|
a => a.CompanyId == companyId && a.IsActive && a.AccountSubType == AccountSubTypeEnum.OtherIncome);
|
||||||
await _accountBalanceService.DebitAsync(gcLiabilityAcctId, remaining);
|
await _accountBalanceService.DebitAsync(gcLiabilityAcctId, remaining);
|
||||||
await _accountBalanceService.CreditAsync(otherIncomeAcctId?.Id, remaining);
|
await _accountBalanceService.CreditAsync(otherIncomeAcctId?.Id, remaining);
|
||||||
}
|
}
|
||||||
@@ -437,7 +437,7 @@ public class GiftCertificatesController : Controller
|
|||||||
private async Task<int?> GetGcLiabilityAccountIdAsync(int companyId)
|
private async Task<int?> GetGcLiabilityAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && a.AccountNumber == "2500");
|
a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "2500");
|
||||||
return acct?.Id;
|
return acct?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,14 +477,14 @@ public class GiftCertificatesController : Controller
|
|||||||
if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
|
if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
|
||||||
{
|
{
|
||||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && (a.AccountSubType == AccountSubTypeEnum.Checking
|
a => a.CompanyId == companyId && a.IsActive && (a.AccountSubType == AccountSubTypeEnum.Checking
|
||||||
|| a.AccountSubType == AccountSubTypeEnum.Cash));
|
|| a.AccountSubType == AccountSubTypeEnum.Cash));
|
||||||
checkingAcctId = acct?.Id;
|
checkingAcctId = acct?.Id;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && a.AccountNumber == "4950");
|
a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "4950");
|
||||||
discountAcctId = acct?.Id;
|
discountAcctId = acct?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -305,8 +305,9 @@ public class InvoicesController : Controller
|
|||||||
&& company?.StripeConnectStatus == StripeConnectStatus.Active;
|
&& company?.StripeConnectStatus == StripeConnectStatus.Active;
|
||||||
|
|
||||||
// Expense accounts for the write-off bad-debt modal
|
// Expense accounts for the write-off bad-debt modal
|
||||||
|
var expenseCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||||
var expenseAccounts = await _unitOfWork.Accounts.FindAsync(
|
var expenseAccounts = await _unitOfWork.Accounts.FindAsync(
|
||||||
a => a.IsActive && a.AccountType == AccountType.Expense);
|
a => a.CompanyId == expenseCompanyId && a.IsActive && a.AccountType == AccountType.Expense);
|
||||||
ViewBag.ExpenseAccounts = expenseAccounts
|
ViewBag.ExpenseAccounts = expenseAccounts
|
||||||
.OrderBy(a => a.AccountNumber).ThenBy(a => a.Name)
|
.OrderBy(a => a.AccountNumber).ThenBy(a => a.Name)
|
||||||
.Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString()))
|
.Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString()))
|
||||||
@@ -2458,7 +2459,8 @@ public class InvoicesController : Controller
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task PopulateBankAccountsAsync()
|
private async Task PopulateBankAccountsAsync()
|
||||||
{
|
{
|
||||||
var accounts = await _unitOfWork.Accounts.FindAsync(a => a.IsActive
|
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||||
|
var accounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId && a.IsActive
|
||||||
&& (a.AccountSubType == AccountSubType.Cash ||
|
&& (a.AccountSubType == AccountSubType.Cash ||
|
||||||
a.AccountSubType == AccountSubType.Checking ||
|
a.AccountSubType == AccountSubType.Checking ||
|
||||||
a.AccountSubType == AccountSubType.Savings));
|
a.AccountSubType == AccountSubType.Savings));
|
||||||
@@ -2473,7 +2475,7 @@ public class InvoicesController : Controller
|
|||||||
private async Task<int?> GetCheckingAccountIdAsync(int companyId)
|
private async Task<int?> GetCheckingAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && (a.AccountSubType == AccountSubType.Checking
|
a => a.CompanyId == companyId && a.IsActive && (a.AccountSubType == AccountSubType.Checking
|
||||||
|| a.AccountSubType == AccountSubType.Cash));
|
|| a.AccountSubType == AccountSubType.Cash));
|
||||||
return acct?.Id;
|
return acct?.Id;
|
||||||
}
|
}
|
||||||
@@ -2482,7 +2484,7 @@ public class InvoicesController : Controller
|
|||||||
private async Task<int?> GetCustomerDepositsAccountIdAsync(int companyId)
|
private async Task<int?> GetCustomerDepositsAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && a.AccountNumber == "2300");
|
a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "2300");
|
||||||
return acct?.Id;
|
return acct?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2490,7 +2492,7 @@ public class InvoicesController : Controller
|
|||||||
private async Task<int?> GetSalesReturnsAccountIdAsync(int companyId)
|
private async Task<int?> GetSalesReturnsAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && a.AccountNumber == "4960");
|
a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "4960");
|
||||||
return acct?.Id;
|
return acct?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2498,7 +2500,7 @@ public class InvoicesController : Controller
|
|||||||
private async Task<int?> GetCustomerCreditsAccountIdAsync(int companyId)
|
private async Task<int?> GetCustomerCreditsAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && a.AccountNumber == "2350");
|
a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "2350");
|
||||||
return acct?.Id;
|
return acct?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2506,7 +2508,7 @@ public class InvoicesController : Controller
|
|||||||
private async Task<int?> GetArAccountIdAsync(int companyId)
|
private async Task<int?> GetArAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var accounts = await _unitOfWork.Accounts.FindAsync(
|
var accounts = await _unitOfWork.Accounts.FindAsync(
|
||||||
a => a.IsActive && a.AccountSubType == AccountSubType.AccountsReceivable);
|
a => a.CompanyId == companyId && a.IsActive && a.AccountSubType == AccountSubType.AccountsReceivable);
|
||||||
return accounts.FirstOrDefault()?.Id;
|
return accounts.FirstOrDefault()?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2517,7 +2519,7 @@ public class InvoicesController : Controller
|
|||||||
private async Task<int?> GetBadDebtAccountIdAsync(int companyId)
|
private async Task<int?> GetBadDebtAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var expenses = await _unitOfWork.Accounts.FindAsync(
|
var expenses = await _unitOfWork.Accounts.FindAsync(
|
||||||
a => a.IsActive && a.AccountType == AccountType.Expense);
|
a => a.CompanyId == companyId && a.IsActive && a.AccountType == AccountType.Expense);
|
||||||
return expenses.FirstOrDefault(a => a.Name.Contains("bad", StringComparison.OrdinalIgnoreCase)
|
return expenses.FirstOrDefault(a => a.Name.Contains("bad", StringComparison.OrdinalIgnoreCase)
|
||||||
|| a.Name.Contains("debt", StringComparison.OrdinalIgnoreCase))?.Id
|
|| a.Name.Contains("debt", StringComparison.OrdinalIgnoreCase))?.Id
|
||||||
?? expenses.FirstOrDefault()?.Id;
|
?? expenses.FirstOrDefault()?.Id;
|
||||||
@@ -2548,9 +2550,9 @@ public class InvoicesController : Controller
|
|||||||
private async Task<int?> ResolveSalesTaxAccountIdAsync(int companyId)
|
private async Task<int?> ResolveSalesTaxAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var taxAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var taxAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.AccountNumber == "2200" && a.IsActive);
|
a => a.CompanyId == companyId && a.AccountNumber == "2200" && a.IsActive);
|
||||||
taxAccount ??= await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
taxAccount ??= await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.AccountType == AccountType.Liability && a.IsActive && a.Name.ToLower().Contains("tax"));
|
a => a.CompanyId == companyId && a.AccountType == AccountType.Liability && a.IsActive && a.Name.ToLower().Contains("tax"));
|
||||||
return taxAccount?.Id;
|
return taxAccount?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2562,9 +2564,9 @@ public class InvoicesController : Controller
|
|||||||
private async Task<int?> GetSalesDiscountAccountIdAsync(int companyId)
|
private async Task<int?> GetSalesDiscountAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var discountAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var discountAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.AccountNumber == "4950" && a.IsActive);
|
a => a.CompanyId == companyId && a.AccountNumber == "4950" && a.IsActive);
|
||||||
discountAccount ??= await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
discountAccount ??= await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.AccountType == AccountType.Revenue && a.IsActive && a.Name.ToLower().Contains("discount"));
|
a => a.CompanyId == companyId && a.AccountType == AccountType.Revenue && a.IsActive && a.Name.ToLower().Contains("discount"));
|
||||||
return discountAccount?.Id;
|
return discountAccount?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2572,7 +2574,7 @@ public class InvoicesController : Controller
|
|||||||
private async Task<int?> GetGcLiabilityAccountIdAsync(int companyId)
|
private async Task<int?> GetGcLiabilityAccountIdAsync(int companyId)
|
||||||
{
|
{
|
||||||
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
a => a.IsActive && a.AccountNumber == "2500");
|
a => a.CompanyId == companyId && a.IsActive && a.AccountNumber == "2500");
|
||||||
return acct?.Id;
|
return acct?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,14 @@ public class JournalEntriesController : Controller
|
|||||||
return RedirectToAction(nameof(SalesTaxPayment));
|
return RedirectToAction(nameof(SalesTaxPayment));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't let a remittance exceed the outstanding liability — overpaying would push Sales Tax
|
||||||
|
// Payable into an abnormal (debit) balance. The 0.005 tolerance absorbs decimal rounding.
|
||||||
|
if (amount > taxAcct.CurrentBalance + 0.005m)
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Payment of {amount:C} exceeds the Sales Tax Payable balance of {taxAcct.CurrentBalance:C}. Enter an amount up to the outstanding liability.";
|
||||||
|
return RedirectToAction(nameof(SalesTaxPayment));
|
||||||
|
}
|
||||||
|
|
||||||
var bankAcct = await _unitOfWork.Accounts.GetByIdAsync(bankAccountId);
|
var bankAcct = await _unitOfWork.Accounts.GetByIdAsync(bankAccountId);
|
||||||
if (bankAcct == null || bankAcct.CompanyId != companyId)
|
if (bankAcct == null || bankAcct.CompanyId != companyId)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user