Added explicit CompanyId == companyId predicates to every tenant-scoped
query in 22 controllers so cross-tenant data leakage is impossible even
if EF Core global query filters are bypassed or misconfigured.
Also fixed ApplicationDbContext.IsPlatformAdmin to correctly return true
for SuperAdmins with no CompanyId claim (break-glass accounts) and when
no HTTP context is present (background services, unit tests), resolving
225 unit test failures that stemmed from the global filter blocking all
in-memory test data.
New MultiTenantIsolationTests class (8 tests) verifies the explicit
predicate layer independently of the global query filters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the ShopWorker and ShopWorkerRoleCost entities, all related DTOs,
mappings, controllers, views, and import/export paths. Worker identity is
now handled entirely through ApplicationUser with per-user LaborCostPerHour.
ShopWorkerRoleCosts table remains in production pending manual data migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Budgeting:
- Budget + BudgetLine entities with Jan–Dec monthly columns per GL account
- BudgetsController: Index, Create, Edit, SetDefault, Copy, Delete
- Copy action rolls a budget forward to a new fiscal year
- Budget vs. Actual report (BudgetVsActual): compares monthly budget amounts to
real P&L by calling GetProfitAndLossAsync once per month; variance shown as
favorable/unfavorable; year + budget selectors in header
- Views: Budgets/Index, Create, Edit with inline annual totals via budget-edit.js
- Nav link + report card on Landing
Year-End Close:
- YearEndClose entity records each closed year + JE reference for audit trail
- AccountsController.YearEndClose GET (history + form) + CloseYear POST
- Close zeroes all Revenue and Expense/COGS account balances into Retained Earnings
via IAccountBalanceService and posts a supporting JE dated Dec 31
- Idempotency: rejects attempt to close an already-closed year
- Pre-close checklist in view to guide the workflow
- Nav link under Finance
Migration AddBudgetsAndYearEndClose applied
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>
- Drop orphan VendorCreditId1 column from VendorCreditApplications (was
scaffolded by EF because WithMany() lacked inverse navigation name;
fixed WithMany() → WithMany(vc => vc.Applications) in ApplicationDbContext)
- Wire EarlyPaymentDiscount fields through full data path: added
EarlyPaymentDiscountPercent/Days to CreateInvoiceDto, hidden inputs to
Invoice Create view, and JS to populate from customer AJAX response
- Add missing [HttpGet] attribute to TaxRatesController.Index
- Document GenerateNow architecture exception with XML rationale
Migration DropOrphanVendorCreditId1 applied. Build: 0 errors, 168 warnings.
Unit tests: 200/200 passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- VendorCredit, VendorCreditLineItem, VendorCreditApplication entities
- VendorCreditStatus enum (Open, PartiallyApplied, Applied, Voided)
- Migration AddVendorCredits: three new tables
- IUnitOfWork/UnitOfWork wired with all three repositories
- VendorCreditsController: Index (status tabs), Create, Details, Post, Apply, Void
- Post action: DR AP, CR each expense line (reverses original expense)
- Apply action: links credit to bill, updates Bill.AmountPaid and bill status
- Views: Index (summary cards + table), Create (dynamic line grid), Details (apply panel)
- Nav: Vendor Credits added to Finance section in _Layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- JournalEntry + JournalEntryLine entities with Draft/Posted/Reversed lifecycle
- JournalEntryStatus enum (Draft, Posted, Reversed)
- Migration AddJournalEntries: two new tables with self-referencing reversal FK
- IUnitOfWork/UnitOfWork wired with JournalEntries + JournalEntryLines repos
- ApplicationDbContext: DbSets, tenant query filters, reversal FK config
- LedgerService: JE lines added as 10th source in GetAccountLedgerAsync and ComputePriorBalanceAsync
- JournalEntriesController: Index (All/Draft/Posted tabs), Create, Details, Post, Reverse, Delete
- Views: Index, Create (dynamic balanced line grid with running debit/credit totals), Details
- journal-entry-create.js: dynamic line management with balance indicator
- Nav: Journal Entries added to Finance section in _Layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the local filesystem path (which required IIS app pool write
access to inetpub\wwwroot\DataProtection-Keys) with SQL Server storage
via IDataProtectionKeyContext. Keys now survive deploys and IIS recycles
without any server-side folder permission setup.
Production continues to use Azure Blob Storage unchanged.
- Add Microsoft.AspNetCore.DataProtection.EntityFrameworkCore 8.0.11 to
Web and Infrastructure projects
- ApplicationDbContext implements IDataProtectionKeyContext
- Migration AddDataProtectionKeys creates DataProtectionKeys table
- Program.cs: non-production path uses PersistKeysToDbContext
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Platform PowderCatalogItem table (IPlainRepository, no tenant filter) with
full spec fields: cure temp/time, finish, color families, clear coat flag,
coverage sq ft/lb, transfer efficiency, IsUserContributed
- Two EF migrations: AddPowderCatalogItem + AddPowderCatalogSpecFields
- PowderCatalogController (SuperAdminOnly): import from Prismatic JSON scrape,
Lookup AJAX endpoint (catalog-first, ranked by SKU exact match), stats view
with Tenant Contributed card
- Unified smart Lookup button on inventory Create/Edit: catalog hit fills all
fields via catalogSnapshot pattern; AI augments cure/finish data from product
URL if subscription enabled; catalog miss falls through to AI lookup
- In-browser label scanner (_LabelScanModal): getUserMedia live camera feed,
jsQR auto-detects QR codes in rAF loop; "Scan Label Text" fallback sends
captured frame to Claude vision via /Inventory/ScanLabel
- ScanLabel endpoint handles both QR URL path (LookupByUrlAsync) and vision
path (ScanLabelAsync); auto-inserts unrecognized products as
IsUserContributed=true; returns wasInCatalog/addedToCatalog flags
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Three-tier SMS gate: platform kill-switch → admin force-disable → plan AllowSms → company opt-in
- CompanySmsAgreement entity records admin acceptance of TCPA terms with IP, user agent, and terms version
- SMS terms of service modal on Company Settings with versioned re-agreement (AppConstants.SmsTermsVersion)
- Dev redirect: non-production SMS routed to Twilio:DevRedirectPhone to protect real customer numbers
- Removed redundant Ready for Pickup SMS (Job Completed covers it)
- Role-based compose modal on job completion: Admin/Manager reviews and edits before send; ShopFloor auto-sends
- Send SMS button on job details for ad-hoc messages (Admin/Manager only)
- SendJobSmsAsync auto-appends STOP opt-out language if missing
- Migrations: AddSmsGating, AddCompanySmsAgreement
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude reviews every active catalog item against the shop's own operating costs
and returns a per-item verdict (below-cost / thin-margin / high / ok) with a
suggested price range, cost floor, and assumptions.
- New entity: CatalogPriceCheckReport (JSON blob, archived per company)
- New service: IAiCatalogPriceCheckService / AiCatalogPriceCheckService
batches items 25 at a time to stay within model context limits
- Two new controller actions: GET AiPriceCheck (view report) + POST RunAiPriceCheck
- AiPriceCheck view: summary cards (counts by verdict), color-coded item cards
with Edit Price link, assumptions detail, and loading spinner on submit
- AI Price Check button added to catalog Index header
- Migration AddCatalogPriceCheckReport applied
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shop floor workers can log in once with a password, enroll a passkey,
and use Face ID / Windows Hello / fingerprint for all future logins.
- UserPasskey entity + AddUserPasskeys migration (Fido2 v4.0.1)
- PasskeyController: RegisterOptions, Register, LoginOptions, Login,
Manage, Remove endpoints
- Login page: platform-aware button (Face ID / Windows Hello / etc.)
hidden automatically if browser doesn't support WebAuthn
- Post-login floating prompt to enroll on first use; session-dismissed
- Passkeys & Biometrics link in user dropdown menu
- Manage page: list registered devices, add new, remove individual
- passkey.js: targeted base64url conversion (only challenge + user.id
+ credential IDs) — fixes "Required parameters missing" error caused
by blindly converting rp.id and other string fields to ArrayBuffers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>