Guard money-account selections; derive account type from sub-type
Item 1 — server-side guard (defense in depth) on payment-source / deposit / reconcilable account selections. New AccountGuard.IsValidMoneyAccountAsync checks the submitted account is active, company-owned, and an Asset or Liability before any GL posting, at: bill RecordPayment, bill Create (payNow), bill EditPayment, BankReconciliation.Create, and deposit Record. The dropdowns already constrain normal users; this rejects tampered/stale POSTs. Per the "trust the operator" decision it still allows A/R etc. (any Asset/Liability) — it only blocks non-money types. Item 2 — account AccountType is now derived from the chosen AccountSubType on create/edit via the new AccountClassification.TypeForSubType (single source of truth, also used by the Create pre-select). The two can no longer disagree, so the sub-type-based debit/credit sign convention is always consistent with the account's type. A read-only sweep of the dev DB found 0 existing mismatches, so no repair tool was built. Audit doc updated: both backlog items marked resolved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -91,18 +91,7 @@ public class AccountsController : Controller
|
||||
if (preSubType.HasValue)
|
||||
{
|
||||
dto.AccountSubType = preSubType.Value;
|
||||
dto.AccountType = preSubType.Value switch
|
||||
{
|
||||
AccountSubType.Cash or AccountSubType.Checking or AccountSubType.Savings
|
||||
or AccountSubType.AccountsReceivable or AccountSubType.Inventory or AccountSubType.FixedAsset
|
||||
or AccountSubType.OtherCurrentAsset or AccountSubType.OtherAsset => AccountType.Asset,
|
||||
AccountSubType.AccountsPayable or AccountSubType.CreditCard
|
||||
or AccountSubType.OtherCurrentLiability or AccountSubType.LongTermLiability => AccountType.Liability,
|
||||
AccountSubType.OwnersEquity or AccountSubType.RetainedEarnings => AccountType.Equity,
|
||||
AccountSubType.Sales or AccountSubType.ServiceRevenue or AccountSubType.OtherIncome => AccountType.Revenue,
|
||||
AccountSubType.CostOfGoodsSold => AccountType.CostOfGoods,
|
||||
_ => AccountType.Expense
|
||||
};
|
||||
dto.AccountType = AccountClassification.TypeForSubType(preSubType.Value);
|
||||
}
|
||||
ViewBag.Inline = inline;
|
||||
if (inline)
|
||||
@@ -151,6 +140,9 @@ public class AccountsController : Controller
|
||||
var account = _mapper.Map<Account>(dto);
|
||||
account.CompanyId = currentUser!.CompanyId;
|
||||
account.CreatedBy = currentUser.Email;
|
||||
// Derive the parent type from the chosen sub-type so the two can never disagree —
|
||||
// a mismatch would post with the wrong debit/credit sign (sign keys off sub-type).
|
||||
account.AccountType = AccountClassification.TypeForSubType(account.AccountSubType);
|
||||
|
||||
await _unitOfWork.Accounts.AddAsync(account);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
@@ -226,6 +218,9 @@ public class AccountsController : Controller
|
||||
}
|
||||
|
||||
_mapper.Map(dto, account);
|
||||
// Keep type consistent with the chosen sub-type (see Create) so the sign convention,
|
||||
// which keys off sub-type, can never be at odds with the displayed account type.
|
||||
account.AccountType = AccountClassification.TypeForSubType(account.AccountSubType);
|
||||
account.UpdatedAt = DateTime.UtcNow;
|
||||
account.UpdatedBy = (await _userManager.GetUserAsync(User))?.Email;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user