Inventory vendor auto-select: match the dropdown off the Manufacturer
field (almost always populated and equal to the vendor for the shop's
distributors) instead of the AI's price-conditional vendorName, which was
only returned when a price was scraped. Centralizes the logic in a shared
inventory-vendor-match.js used by catalog lookup, AI lookup, label scan,
and manual entry; skips brands sold by multiple distributors (PPG, KP
Pigments) so those stay manual.
Account dropdowns filtered by sub-type now filter by parent AccountType,
so accounts a company classifies under a non-standard sub-type still
appear: Inventory account (Asset), AP account (Liability), pay-from/bank
and Bank Reconciliation pickers (Asset + Liability).
Deposit account is now a user-selectable dropdown on the Job and Quote
deposit modals (Asset + Liability accounts) instead of a silent auto-pick
of the first Checking/Cash account; falls back to the old behavior when
left blank, and validates the chosen account belongs to the company.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the read-path defense-in-depth pass flagged in the accounting audit:
every Accounts lookup in a controller now carries an explicit CompanyId predicate,
matching the standing rule in CLAUDE.md ("every FindAsync/GetAllAsync must include
an explicit CompanyId"). ~19 lookups across 12 controllers:
- Tier 1 (write-path): AccountsController duplicate account-number check (Create/Edit)
- Tier 2 (dropdowns/lists): Accounts (Index/year-end/parent), BankReconciliations,
Bills (bank list + receipt scan + suggest), Budgets, CatalogItems, Expenses,
FixedAssets, Inventory, JournalEntries chart dropdown, Vendors
- Tier 3 (accountIds.Contains display maps): JournalEntries/Reports/VendorCredits
detail views, scoped via the in-scope entity's CompanyId for uniformity
companyId source per controller: _tenantContext where available, else the in-scope
entity's CompanyId, else the current user. Build clean; 291 unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- AppConstants: add Accountant to CompanyRoles; add CanManageBills and
CanManageAccounting to Policies
- ApplicationUser: add CanManageBills and CanManageAccounting bool fields
- UserManagementDtos: expose new fields in all three DTOs
- ClaimsPrincipalFactory: emit ManageBills and ManageAccounting claims
- Program.cs: add CanManageBills and CanManageAccounting policies;
update CanManageInvoices, CanViewReports, CanManagePurchaseOrders,
and CanManageVendors to auto-pass for Accountant role
- BillsController: replace CanManageInventory with CanManageBills on
all write actions (correct policy — bills are not inventory)
- BankReconciliationsController: replace CanManageJobs with
CanManageAccounting on write actions
- CompanyUsersController: add Accountant to validCompanyRoles (both
Create/Edit), legacyRole switch, and all permission assignment blocks
- Create/Edit views: add Accountant option to role dropdown; add
CanManageBills and CanManageAccounting checkboxes; JS auto-checks
financial permissions when Accountant role is selected
- Migration AddAccountantRolePermissions: adds columns + backfills
CanManageBills=1 and CanManageAccounting=1 for all CompanyAdmin users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Feature 7: Bank Rec Auto-Match — AiSuggestMatches endpoint scores uncleared
transactions vs statement ending balance; AI Auto-Match panel in Reconcile.cshtml
with confidence highlights and Apply All button.
Feature 8: Late Payment Prediction — PredictLatePayments endpoint scores open AR
customers by risk (high/medium/low) using historical avg-days-to-pay + late rate;
rendered as badge table in AR Aging view via ar-aging-ai.js.
Feature 9: Natural Language Financial Queries — FinancialQuery GET page + RunFinancialQuery
POST; 12-month context snapshot pre-loaded; answers grounded in real data with
supporting facts, follow-up suggestions, session history, and example chips.
Feature 10: Recurring Bill Detection — RunRecurringDetection scans 12 months of bills
for vendor payment patterns (monthly/quarterly/annual); card grid view in Bills/RecurringDetection.cshtml
with confidence badges, next-expected-date, and suggested actions.
Supporting: 4 new DTO groups in AccountingAiDtos.cs, 4 method signatures in
IAccountingAiService.cs, 4 implementations in AccountingAiService.cs, 4 new
AiFeatures constants, 2 new Landing page AI report cards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Invoice Write-Off: WriteOff POST action in InvoicesController posts bad-debt JE
(DR bad debt expense / CR AR), reduces customer balance, marks invoice WrittenOff;
write-off modal added to Invoice Details view with expense account selector
- Fixed Assets: FixedAsset + FixedAssetDepreciationEntry entities with straight-line
depreciation; FixedAssetsController (Index/Create/Edit/Details/PostDepreciation/Delete);
PostDepreciation auto-generates one JE per asset per period, skips already-posted,
fully-depreciated, and disposed assets; full CRUD views + nav link
- Period Locking: Company.BookLockedThrough field; AccountingPeriodValidator static helper;
lock check added to JE Post and Bill Create (blocks backdating into closed periods);
SetPeriodLock action + date picker UI in Company Settings Accounting section
- 1099 Tracking: Is1099Vendor flag on Vendor entity + DTOs; checkbox in Create/Edit views;
TaxReporting1099 report action + view lists payments by year, flags vendors >= $600;
report card added to Reports Landing
- Migration AddFixedAssetsLockAnd1099 applied
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AccountingDropdownHelper: wired into BillsController and ExpensesController,
replacing 35-40 lines of duplicated DB queries per controller
- AppConstants.StatusCodes: added Job.* and Quote.* constants to replace all
magic status strings across Jobs, Quotes, Appointments, OvenScheduler,
AiQuickQuote, QuoteApproval, and AccountingDropdownHelper
- AccountingRules: extracted IsNormalDebitBalance into shared Infrastructure
helper; removed duplicate private method from AccountBalanceService and
LedgerService (~50 lines deleted)
- AccountDataExportController: extracted 9 Fetch*Async methods (superset of
includes) so Add*Sheet and Build*Csv no longer duplicate DB queries; each
entity is queried once regardless of whether XLSX or CSV format is requested
- BillsController.Create and ExpensesController.Create wrapped in
ExecuteInTransactionAsync; blob uploads moved after commit to keep
financial data atomic and prevent orphaned blobs from rolling back
- Number generators (Appointments, CreditMemo, OvenBatch) fixed from full-table
GetAllAsync to prefix-filtered FindAsync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Quotes: ad-hoc email modal on Quote Details lets staff send to an address not on file;
QuotesController passes overrideEmail through to NotificationService
- Quotes/Details view: SMS consent display, email/SMS send button state based on consent
- Accounting module: AccountingDisplayHelpers for consistent ledger formatting;
AccountsController + Accounts views improvements; AccountingEnums additions
- Bills/Expenses: AI account categorization fixes in BillsController and ExpensesController
- InventoryAiLookupService: TDS cure fallback no longer fires on AiAugmentFromUrl path
(LookupByUrlAsync already has it built in — was double-fetching)
- PdfService: quote/invoice PDF updates
- PricingCalculationService: minor pricing logic fix
- QuoteProfile: mapping updates for new quote fields
- ApplicationDbContextModelSnapshot: catches up to all 4 migrations in this branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migrated InvoicesController, QuotesController, JobsController, BillsController,
PurchaseOrdersController, and CustomersController to route all data access
through IUnitOfWork typed/generic repositories instead of injecting
ApplicationDbContext directly.
New typed repositories added: IJobRepository (GetScheduledJobsForDateAsync,
GetActiveJobsForMobileAsync, LoadForCostingAsync), INotificationLogRepository
(GetLatestForJobAsync, GetAllForJobAsync), IQuoteRepository (GetItemsWithCoatsAsync
with CatalogItem eager load + AsNoTracking), and IJobRepository.GetOrphanedConversionJobAsync.
All EF complex include chains relocated into repository methods; controllers now
call named query methods rather than composing raw IQueryable chains.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>