Compare commits

..

70 Commits

Author SHA1 Message Date
spouliot 9bbe1e4e27 Merge master into dev: quote stat cards Converted fix 2026-06-15 16:29:59 -04:00
spouliot cbfd3e1bbd Merge hotfix/quote-stats-converted-mismatch: exclude Converted quotes from Quotes Index stat cards 2026-06-15 15:47:25 -04:00
spouliot 45d9614c47 Fix Quotes Index stat cards counting Converted quotes hidden from the list
The Quotes Index stat strip (OPEN / APPROVED / TOTAL VALUE) summed every
non-deleted quote, while the default list hides Converted quotes. A quote
converted to a job (whose deletion is blocked by the linked job) therefore
stayed invisible in the list but kept inflating the cards -- e.g. a blank
list showing "1" and a non-zero total value.

GetIndexStatsAsync now excludes the Converted status so the cards reflect
the same population as the default list. Converted value is intentionally
dropped from the quote pipeline because it carries forward on the job
(counting it in both would double-count the same dollars).

Also adds an explicit CompanyId predicate to GetIndexStatsAsync (defense in
depth) -- it was the only Quote query in the typed repo relying solely on
the global tenant filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:46:39 -04:00
spouliot 32a95052fa Remove accidentally-committed publish-output/ and stray root artifacts
Deletes the committed dotnet publish output folder (434 files: DLLs,
bundled static assets) plus 73 stray root files (old *_FIX/*_SUMMARY
docs, .bak files, loose .sql scripts, deploy.zip, screenshots) and a
few scripts/. Repo housekeeping to reclaim disk space; no src/ or
wwwroot/ files touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:09:11 -04:00
spouliot c16b2445bc Hotfix: Company Settings save button not responding
Save button was type=submit, so HTML5 form validation silently blocked
the submit event and nothing happened on click. Switch to type=button
with an explicit click handler. Also replace AutoMapper Map() with
explicit property assignment so EF reliably detects the mutations, and
re-enable the button in showButtonSuccess() after a successful save.

Cherry-picked CompanySettings hunks from dev commit 0b839d0746 as a
targeted production patch off v2026.06.09.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:04:42 -04:00
spouliot c4625ba28a Security: document unpatched System.Security.Cryptography.Xml advisory
GHSA-37gx-xxp4-5rgx and GHSA-w3x6-4m5h-cxqf (XML signature vulns) affect
8.0.2 transitively. No patched version exists in the NuGet feed yet — 9.0.0
is also flagged. Tracked in Directory.Build.props for re-check when a fix ships.

System.Net.Http 4.1.0 and System.Security.Cryptography.X509Certificates 4.1.0
are false positives: same NCalc2 -> Antlr4 -> NETStandard.Library 1.6.0 chain
already documented; .NET 8 BCL provides the runtime versions.

Microsoft.Build / NuGet.* are build-tooling-only, not deployed to production.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 22:17:46 -04:00
spouliot 9c1beab49e Security: add missing class-level [Authorize] on ReleaseNotesController and KioskController
ReleaseNotesController had [Authorize] only on Index(), leaving the class
unprotected at declaration level — any future unannotated action would be
publicly accessible.

KioskController had no class-level auth, meaning PushSmsConsent() and
CancelSmsConsent() (staff-only POST actions) were reachable by anonymous
callers. [AllowAnonymous] on the existing tablet/intake actions still
overrides correctly, so the customer-facing kiosk flow is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 21:44:59 -04:00
spouliot aeec899cf2 Performance: push ORDER BY/TAKE into SQL for hot-path reads
- IInAppNotificationRepository: typed repo with GetPagedAsync, GetRecentAsync, GetUnreadAsync
  — bell dropdown no longer loads all notifications then slices in C#
- Add compound indexes on InAppNotifications(CompanyId, IsDeleted, CreatedAt) and
  (CompanyId, IsDeleted, IsRead); ContactSubmissions(CompanyId, IsDeleted, CreatedAt)
- PlainRepository.GetAllAsync/FindAsync: add AsNoTracking (Announcements, Tips, ReleaseNotes)
- AiUsageReportController: replace GetAllAsync + C# Where with FindAsync (SQL-level filter)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 21:34:12 -04:00
spouliot 54defc158f Multi-tenancy hardening: explicit companyId on all typed repository methods
All typed repository methods that previously relied solely on global query
filters now require an explicit companyId parameter, providing defense-in-
depth so IgnoreQueryFilters calls cannot leak cross-tenant data.

- IBillRepository/BillRepository: GetForIndexAsync, LoadForViewAsync,
  LoadForEditAsync, GetLastBillNumberAsync, GetLastPaymentNumberAsync,
  GetForDateRangeAsync all scoped to companyId
- IJobRepository/JobRepository: LoadForDetailsAsync, LoadForEditAsync,
  LoadForStatusChangeAsync, GetChangeHistoryAsync,
  LoadForTemplateSnapshotAsync, GetReworkJobCountAsync
- IQuoteRepository/QuoteRepository: LoadForDetailsAsync,
  GetChangeHistoryAsync, GetItemsWithCoatsAsync
- IInvoiceRepository/InvoiceRepository: LoadForViewAsync
- ICustomerRepository/CustomerRepository: LoadForDetailsAsync
- INotificationLogRepository/NotificationLogRepository: all 6 FK methods
- BillsController: ITenantContext injected, all call sites updated
- AccountingExportController, InvoicesController, JobsController,
  JobTemplatesController, QuotesController: call sites updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 19:12:23 -04:00
spouliot 8f11e00a0a Merge duplicate powder lines on dashboard order queue
When multiple jobs need the same powder, the 'Powder in Queue to be
Ordered' panel now collapses them into a single line (summed lbs) rather
than showing one row per coat. 'Mark as Ordered' marks all contributing
coats at once and injects each into the 'Awaiting Receipt' panel
individually so per-coat receiving still works unchanged.

- Add PowderOrderJobRefDto; PowderOrderLineDto gains CoatIds + Jobs lists
  (scalar CoatId/JobId/etc. become computed accessors for backward compat)
- MapPowderOrderGroupsMerged: secondary GroupBy on (ColorName, ColorCode,
  Finish, SKU) within vendor group for the 'needed' panel
- MapPowderOrderGroups kept per-coat for the 'awaiting receipt' panel
- MarkPowderOrdered accepts comma-separated coatIds, returns coats array
- Dashboard view: Customer column loops job refs for merged rows; JS posts
  coatIds and iterates data.coats to populate awaiting-receipt panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:59:10 -04:00
spouliot a21c05f655 Expand demo seed: 178 inventory items + 30 vendors
Inventory (11 → 178):
- 101 total powders: 6 core + 55 Prismatic + 20 Columbia + 13 Tiger Drylac + 9 Sherwin-Williams
- 77 supplies: 21 masking, 16 chemicals, 16 abrasives, 15 hanging hardware, 9 PPE
- ForceRemoveAll path now deletes all inventory for the company (not just
  the 11 enumerated SKUs), since transactions are pre-swept before this block

Vendors (5 → 30):
- Tiger Drylac, Sherwin-Williams Powders, Eastwood (powder suppliers)
- Clemco, Triangle Abrasives, Airgas, Linde (blasting/gases)
- Duke Energy, AT&T, Spectrum, Raleigh Electric, Carolina Industrial Water (utilities)
- Safety-Kleen, Raleigh Waste (environmental)
- Work N Gear, HD Supply, Carolina Office, First Insurance (services)
- Triangle Commercial Properties LLC (landlord — shop lease with address + terms)
- Fastenal, MSC, McMaster-Carr, Uline, Amazon Business, Lowe's Pro, NAPA (supply chain)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 10:06:41 -04:00
spouliot 1e5510477a Allow fractional quantities (< 1) in item wizard
Catalog, calculated, generic, formula, and AI item types now accept
decimal quantities (e.g. 0.25 for a quarter of a catalog set). Sales/
merchandise items remain whole-number only.

- Input min changed from 1 to 0.01; step="0.01" added where missing
- All parseInt reads on quantity inputs changed to parseFloat so values
  like 0.25 aren't truncated to 0 before being stored in wz.data
- Server-side Quantity is already decimal on all relevant DTOs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 09:41:06 -04:00
spouliot 6eb7be0193 Demo reset + dev banner suppression for DEMO company
- DemoController: company-code-gated reset action (DEMO only, CSRF protected)
- SeedDataService.Remove: FK-safe topological pre-sweep, all deletes scoped to companyId
- SeedDataService: clock entries, extra seed data, updated customer/worker/job-status seeders
- CompanySettingsController + Index.cshtml: Reset Demo Data button for DEMO company users
- ReportsController + FinancialReportService: supporting report fixes
- _Layout.cshtml: suppress env banner when current company is DEMO (all auth paths)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 09:26:40 -04:00
spouliot 7735fe3cce Demo data realism + invoice resend via SMS on any status
Seed data fixes:
- Fix EF interceptor: no longer overwrites explicitly-set CreatedAt on Added
  entities — root cause of all "same month" chart issues
- Customer seeder: generates 15 customers/month from Jan → current month;
  keeps 10 commercial anchors in deterministic order for job seeder index map
- Invoice seeder: historical range bumped from 2→8 paid invoices/month so
  P&L shows consistent profit (~$5,200 collected vs ~$4,200 monthly expenses)
- Month -1 bumped to 7 paid invoices to stay above expenses
- Jobs: set UpdatedAt to historical event date so analytics don't need null fallback
- Analytics (ReportsController): use CompletedDate ?? UpdatedAt ?? CreatedAt for
  revenue chart grouping; fixes empty Revenue Trend charts on Overview/Revenue tabs
- SeedDataService: inject IAccountBalanceService; auto-recalculate account balances
  after seeding; patch checking/savings opening balances unconditionally on reset
- Customer list: sort by CompanyName ?? ContactLastName so individuals and
  commercial accounts interleave instead of appearing as two blocks

Invoice resend:
- ResendInvoice action now accepts sendEmail + sendSms parameters; SMS-only
  resend no longer requires an email address on file
- Ensures PublicViewToken exists before SMS so the view link is always valid
- canResend in Details view now allows Paid invoices (removed != Paid guard)
- Resend button shows channel-choice modal when customer has both email + SMS,
  direct SMS button when SMS only, or email button when email only
- New #resendChannelModal mirrors the Send channel modal but posts to ResendInvoice
- resendInvoice() JS updated to pass sendEmail/sendSms query params

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:20:04 -04:00
spouliot 249128e852 Fix Reset Demo Company: full wipe mode + missing removal categories
Root cause: fingerprint-based removal failed on databases seeded with
older code (different emails/SKUs); plus Vendors, Named Ovens, and
Appointments had no removal path at all.

- Add ForceRemoveAll flag to RemoveSeedDataOptions: when true, all
  removal blocks delete by CompanyId instead of fingerprint matching
- Customers block: ForceRemoveAll deletes all company customers
- Workers block: ForceRemoveAll deletes all users with CompanyRole=Worker
- New Vendors block (triggered by options.Vendors || ForceRemoveAll)
- New NamedOvens (OvenCost) block (triggered by options.NamedOvens || ForceRemoveAll)
- New Appointments block (triggered by options.Appointments || ForceRemoveAll)
- ResetDemoCompany: set ForceRemoveAll=true and enable all new flags so
  every re-seedable table is wiped clean before re-seeding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 22:49:30 -04:00
spouliot c0e4a66126 Phase 4: Past appointments + AI prediction demo data
- Appointments: add ~25 past appointments (last 90 days) with Completed,
  Cancelled, No Show, and Rescheduled statuses; completed records carry
  ActualStartTime/ActualEndTime with realistic variance; cancel/no-show
  notes explain why; customer label falls back to ContactFirst/LastName
  for residential customers
- Fix future appointment title for residential customers (was always using
  CompanyName which is null for individuals)
- New SeedDataService.AiPredictions.cs: seeds 8 AiItemPrediction records
  (varied complexity/confidence/tags/reasoning) and attaches them to the
  first 8 eligible QuoteItems, marking those items IsAiItem=true; 3 of 8
  have UserOverrodeEstimate=true for AI Accuracy report demo
- SeedDataService.cs: wire SeedAiPredictionsAsync after Invoices
- Remove.cs: collect QuoteItem.AiPredictionId FKs before deleting items,
  then delete orphaned AiItemPrediction records after quotes are removed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 22:40:12 -04:00
spouliot dbd39a9fe5 Phase 3: AR/AP aging buckets, PO seeder, Bills vendor fix
- Bills.cs: replace aceHardware/fastenal lookups with grainger/harbor/localSupply
  to match Phase 1 vendor renames; update all vendor invoice number prefixes
- Bills.cs: add 3 AP aging-bucket bills (30-60, 61-90, 90+ days overdue) so all
  four AP aging buckets are populated for report demos
- Invoices.cs: add 3 more overdue invoices (31-60, 61-90, 90+ day AR buckets)
  alongside the existing 21-day overdue; total now 29 invoices
- New SeedDataService.PurchaseOrders.cs: 7 POs — 3 Received (historical), 2
  Submitted (in-flight), 2 Draft (pending approval); links to inventory items
  where available
- SeedDataService.cs: wire SeedPurchaseOrdersAsync after Vendors seeder
- Remove.cs: add PO + POItem cleanup inside Bills removal block (two-step ID
  fetch to avoid nested LINQ translation issues)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 22:29:31 -04:00
spouliot 584664e7c8 Demo seed Phase 2: workers, time entries, maintenance records
- 5 named shop workers seeded as ApplicationUser (Employee role):
  Mike Sanders (Coater), Jake Wilson (Sandblaster), Sarah Brooks (Inspector),
  Tyler Green (General), Chris Mason (Lead) — @pcldemo.com fingerprint domain
- Job time entries seeded for all in-progress and completed jobs;
  Worker Productivity report will have data from day one
- Maintenance history seeded per equipment: 2 completed records + 1 upcoming
  scheduled + 1 overdue record on Pressure Pot for overdue alert demo
- Equipment renamed to spec names: Main Batch Oven, Small Batch Oven, Powder
  Coating Booth, Blast Cabinet, Pressure Pot Blaster, Air Compressor, Wash
  Station, Forklift (replaced Overhead Conveyor which wasn't in spec)
- RemoveSeedDataOptions.Workers added; Remove.cs cleans up workers + time
  entries on Demo Reset; SeedDataController resets workers in ResetDemoCompany

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 22:20:04 -04:00
spouliot 1255bc0670 Demo seed Phase 1: NC identity, spec inventory, revenue targeting
- Customers: 10 NC commercial (Carolina Fabrication, Apex Motorsports, Triangle
  Offroad, Smith Welding, Raleigh Architectural Metals, etc.) + 17 residential,
  all anchored to Raleigh-Durham area for cohesive tutorial identity
- Inventory: 6 spec powders (Gloss Black, Matte Black, Super Chrome, Candy Red,
  Signal White, Illusion Purple) + 5 consumables (Tape, Silicone Plugs, Hooks,
  Acetone, Blast Media); 2 low-stock + 1 out-of-stock for dashboard alerts
- Vendors: updated to spec (Prismatic Powders, Columbia Coatings, Harbor Freight,
  Grainger, Local Industrial Supply)
- Quotes: 35 quotes (was 20) with 5-status distribution; dates span 5-6 months
- Jobs: 50 jobs (was ~32) with per-customer price ranges so Revenue by Customer
  report shows realistic Pareto curve (Carolina Fabrication largest, etc.)
- Remove.cs: fingerprints updated for all 27 new customer emails + 11 new SKUs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 22:12:47 -04:00
spouliot 01f6897d08 Scale demo seed data down for tutorial recordings
Customers: 100 → 27 (15 commercial across auto/industrial/architectural/
fitness/marine/energy, including 2 tax-exempt govts; 12 individuals)

Quotes: 75 → 20; date range extended to 4-6 months (was 90 days);
status distribution adjusted proportionally (2 draft, 3 sent, 10 approved,
3 rejected, 2 expired)

Jobs: fixed 50-loop → per-customer 0-5 jobs (~32 total); jobIdx cycles
all 16 statuses globally so every status is visible; creation dates spread
across 1-5 months for in-progress/early jobs, 2-6 months for completed jobs

SeededCustomerEmails updated to match new 27-customer set (added
gnelson@email.com and carol.evans@email.com)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 21:56:32 -04:00
spouliot 72382a5dd5 Fix demo reset: wipe bills/expenses, fix apostrophe display bug
- Add Bills and Expenses flags to RemoveSeedDataOptions
- RemoveSeedDataAsync: delete BillPayments + BillLineItems + Bills, then
  Expenses for the company when those flags are set
- ResetDemoCompany action: enable Bills=true and Expenses=true so all
  seeded AP data is cleared before re-seeding (was skipping on second reset)
- Fix apostrophe in success message (was &apos; in C# string, double-encoded
  by Razor to literal &apos; on screen)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 21:48:43 -04:00
spouliot 86a293a927 Add one-click Demo Company reset for tutorial recording prep
New ResetDemoCompany POST action wipes all seeded data (customers, jobs,
quotes, invoices, inventory, equipment, catalog, pricing tiers, operating
costs) from the DEMO company and immediately re-seeds with fresh records
dated relative to today. Seed data already used relative dates so every
reset produces a realistic, current-looking dataset.

View adds a red "Reset Demo Company" card at the top of the Seed Data page,
visible only when the DEMO company exists. Single button with confirm dialog;
shows exactly what will be wiped and what will be preserved (user accounts,
company settings, lookup tables).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 21:34:57 -04:00
spouliot 35264e6b2a Fix preferred powder selection and expand company settings export
- customer-details.js: encode double quotes in JSON.stringify output as &quot; so onclick attributes parse correctly when powder names contain double quotes
- ToolsController: add company_settings CSV to ExportAllCsv ZIP archive (was missing entirely)
- ToolsController: add ~30 missing fields to GenerateCompanySettingsCsv — AccountingMethod, timeclock settings, all shop capability/blast/coat rate fields, complexity surcharge percents, pricing mode, invoice number prefix, email-from fields, per-event notification flags, payment reminder settings, document accent colors/terms/footer notes, kiosk intake output
- Update GenerateCompanySettingsTemplate to match so import template stays in sync with export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 21:12:49 -04:00
spouliot 0b839d0746 Fix Company Settings save, invoice PAID stamp, and purge script
- Company Settings: switch save button from type=submit to type=button
  to bypass HTML5 form validation blocking the submit event; replace
  AutoMapper Map() with explicit property assignment so EF change
  tracking reliably detects mutations; fix showButtonSuccess() never
  re-enabling the button after a successful save
- Invoice PDF: move PAID stamp into the header row as a centered middle
  column so it sits between the company and invoice blocks without
  adding height to the document
- Purge script: use business-date fields instead of CreatedAt so
  imported records (which all share today's CreatedAt) are correctly
  filtered by actual transaction dates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 17:36:15 -04:00
spouliot 66c3febd7a Move invoice PAID stamp inline to header; add email to company block
- Remove watermark overlay layer; PAID badge now sits centered between
  the header row and the accent rule so it never obscures line items
- Add PrimaryContactEmail to company info block in header
- Remove ComposePaidStamp helper (no longer needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:39:11 -04:00
spouliot b8057295ec Redirect emails to dev address in non-production; fix PAID stamp color
- EmailService: add RedirectIfNonProd() mirroring SmsService pattern;
  reads SendGrid:DevRedirectEmail and redirects all outbound email in
  non-production so real customers are never contacted on local/dev
- appsettings.json: set DevRedirectEmail to spouliot@scppowdercoating.com
- PdfService: revert Opacity() (not in QuestPDF 2024.12.3); use
  Colors.Green.Lighten2 for stamp + border to achieve lighter look

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:32:01 -04:00
spouliot 14d6c82839 Make invoice PAID stamp smaller and semi-transparent
Reduce font 80→52, border 5→3, add 35% opacity so stamp no longer
obscures line items on dense invoices.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:27:33 -04:00
spouliot db4b73013a Simplify inventory label: combine header and scan hint, remove dashed footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:24:49 -04:00
spouliot e313149f08 Fix inventory label duplicate color/finish display
- Suppress ColorName line if it matches the item Name (powders use
  color name as their item name, causing it to show twice)
- Suppress Finish if already contained in the item Name
- Always show Manufacturer regardless of whether it is populated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:07:43 -04:00
spouliot 82fb48f7a5 Patch export/import for missing fields; add CustomerContacts export
- DataExportController + AccountDataExportController: add ProjectName to
  Jobs, Quotes, Invoices (XLSX + CSV); add LeadSource + ShipTo fields to
  Customers (XLSX + CSV); add CustomerContacts sheet/CSV (new)
- Both export views: add Customer Contacts checkbox (checked by default)
- CustomerImportDto: add LeadSource + ShipTo* fields
- JobImportDto: add ProjectName
- QuoteImportDto: add ProjectName
- InvoiceImportDto: add Project Name (dual-name alias for round-trip)
- CsvImportService: wire all new import fields to entity creation;
  also patch invoice update path for ProjectName
- Add scripts/purge_imported_data.sql (dry-run T-SQL for data cleanup)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:14:27 -04:00
spouliot 427c52a499 Fix Ready for Pickup filter returning no results
The Ready pill passed searchTerm=ReadyForPickup which did a text search —
"readyforpickup" (no spaces) never matched the display name "Ready for Pickup".
Converted to statusGroup=ready and added the corresponding controller case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 14:39:35 -04:00
spouliot d92266b027 Fix empty state showing Create First button when a filter is active
Jobs: use AllJobCount (global total) to distinguish truly-empty from
filter-returned-nothing; show Clear Filters button in the latter case.
Quotes: expand the filter-active check to include tagFilter and statusCode,
which were missing from the condition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 14:20:20 -04:00
spouliot 750e1b1c5b Fix preferred powder search dropdown not appearing
Inline display:none!important on the results div blocked all CSS rules
from showing it, including the :not(:empty) trick. Switched to explicit
JS show/hide so the dropdown is reliably visible after typing 2+ chars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 13:59:14 -04:00
spouliot 94a89ee175 Add CRM features: Additional Contacts, Lead Source, Ship-To Address; update Help docs
- New CustomerContact entity + migration (AddCustomerContactsAndCrmFields)
- Customer.LeadSource + ShipToAddress/City/State/ZipCode/Country fields
- Additional Contacts card on Customer Details with AJAX add/edit/delete
- Lead Source dropdown on Create/Edit; Ship-To section on Create/Edit
- Customer Details: side-by-side billing/ship-to when ship-to is set
- Help docs: Customers (contacts, ship-to, lead source, preferred powders, outstanding pickups)
- Help docs: Jobs (clone job, project name), Quotes (project name), Invoices (project name), Inventory (low stock clickable filter)
- HelpKnowledgeBase.cs updated for all features above

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 12:46:08 -04:00
spouliot 711cd01cd3 Add CRM features: Outstanding Pickups, Customer Notes, Clone Job, Preferred Powders
- Outstanding Pickups card on Customer Details shows jobs awaiting pickup with age badges
- Customer Notes log: inline add/delete notes with important flag, AJAX-backed
- Clone Job action on Jobs controller; Repeat Last Job button on Customer Details quick actions
- Preferred Powders per customer: typeahead inventory search, AJAX add/remove
- CustomerPreferredPowder entity + migration; unit tests for CRM stats/timeline logic
- Fix EF Core concurrency bug: parallel Task.WhenAll FindAsync replaced with sequential awaits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 19:59:32 -04:00
spouliot 7cbae31916 Fix invoice ProjectName not pre-filling on edit; add to Details view
Edit GET now falls back to job.ProjectName for invoices created before the
column was added. Details view shows Project Name alongside Customer PO.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 08:58:09 -04:00
spouliot 9367e358d9 Add Project Name field to invoice create and edit forms
Stores ProjectName on the Invoice entity (previously only inherited from the
linked job at display time). Pre-fills from the job when creating from a job.
Migration: AddInvoiceProjectName.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 08:50:02 -04:00
spouliot 9f1460c9c0 Make Low Stock stat card clickable to filter inventory by low stock items
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:48:04 -04:00
spouliot 94e536178c Add optional Project Name field to quotes, jobs, and printed documents
- Add ProjectName (nvarchar 100, nullable) to Quote and Job entities;
  migration AddProjectNameToQuotesAndJobs applied
- Add ProjectName to all relevant DTOs: QuoteDto/Create/Update,
  JobDto/List/Create/Update, InvoiceDto (mapped from Job.ProjectName
  via AutoMapper so the invoice PDF picks it up without a separate column)
- Form field added after Customer PO in Quote Create/Edit and Job Create/Edit
- CreateJobFromQuote copies ProjectName from quote to job automatically
- Details views (Quote and Job) display Project when set
- Printable quote PDF: Project row in the quote details block
- Work order: Project row in customer/job info section
- Invoice PDF: Project shown in the Job Reference block alongside Job # and PO #

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 14:48:28 -04:00
spouliot 456d054229 Fix prospect quote conversion losing the job; add reply-to in email footer
QuotesController — ConvertToCustomer POST was wrongly setting the quote
status to 'Converted' (which means a job exists) and redirecting to the
customer page with no job created. The quote then disappeared from the
default list filter and the user had no way to create the job without
hunting for it. Fix: leave the quote at 'Approved' after customer
creation and redirect back to the quote details page with a toast
prompting the next step. 'Converted' status is now set exclusively by
CreateJobFromQuote when a job actually exists.

NotificationService — add tenant reply-to email address as a visible
line in the email footer so customers who ignore or whose mail client
doesn't honour the Reply-To header still have a clear address to contact.
Also adds Warning-level logging when no reply-to is configured for a
company so future routing issues are diagnosable from app logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 10:35:48 -04:00
spouliot f38a1e3273 Add Reply-To diagnostic logging to GetEmailFromAsync
Logs a Warning when no Reply-To email is configured for a company
(so the logs show why replies land at the platform sender address)
and a Debug entry when one is set, making future send issues
diagnosable without needing the SendGrid Activity API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 11:08:45 -04:00
spouliot 03b425a12f Update blast rate tests to match nozzle-primary formula
Remove the CFM=0→zero test (CFM no longer in the formula path).
Update expected values to match the new nozzle-primary tables and
corrected TierDefaults CFM/nozzle pairings. Add WetBlasting and
RustAndScale substrate coverage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 16:21:39 -04:00
spouliot 8453449833 Recalibrate blast rate formula from industry reference tables
Replace the inaccurate CFM-based formula with nozzle-primary tables
sourced from industry standard abrasive blast cleaning references:
- Pressure pot: midpoints averaged from two reference tables
  (#4 nozzle: 115 sqft/hr, #5: 175 sqft/hr, etc.)
- Siphon cabinet: dedicated siphon cabinet reference table
  (#4 nozzle: 125 sqft/hr, #3: 75 sqft/hr, etc.)
- SiphonPot: 80% of pressure pot rate (open gravity feed, no enclosure)
- WetBlasting: 60% of pressure pot rate (water-media reduces velocity)

CFM is removed from the rate formula entirely — nozzle size determines
throughput and CFM draw, so CFM is a consequence of nozzle choice, not
an independent variable. Override field still bypasses formula for shops
that have measured their own throughput.

Also corrects TierDefaults nozzle/CFM pairings which were mismatched
(e.g. Small tier had 40 CFM assigned to a #5 nozzle that needs 150 CFM).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 16:12:45 -04:00
spouliot ad986561c9 Fix AI quote blast rate: single formula path, correct client preview
Root cause: company-settings-lookups.js had its own baseByCfm/multiplier
tables that were completely different from ShopCapabilityCalculator.cs,
so the UI showed an inflated rate (e.g. 82 sqft/hr) while the AI prompt
received the server-computed rate (e.g. 9 sqft/hr).

- Add CompanySettingsController.DeriveBlastRate endpoint — thin GET that
  calls ShopCapabilityCalculator directly; now the single formula path
- Delete all client-side formula code (baseByCfm, multiplier tables,
  deriveBlastRate) — ~30 lines removed
- Modal live preview calls /CompanySettings/DeriveBlastRate with 250ms
  debounce instead of computing locally
- Blast setup table uses setup.derivedRate from GetBlastSetups (already
  server-computed) instead of recalculating client-side
- QuotesController.AiAnalyzeItem: when no blast setup is explicitly
  selected, fall back to the company's default blast setup so the
  configured rate is always used

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 15:57:46 -04:00
spouliot 0d5553f3b2 Fix dark mode hover colors in coat/powder dropdown menus
Replace all hardcoded light-mode colors (#f0f4ff, #e8eeff, #fff8e1, #fff)
with Bootstrap CSS variables (--bs-secondary-bg, --bs-primary-bg-subtle,
--bs-warning-bg-subtle, --bs-body-bg) so dropdown containers and hover
states render correctly in both light and dark mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 15:12:24 -04:00
spouliot 87bbf158a4 Fix material usage logging: remaining weight mode, edit modal, and consolidate duplicate logic
- InventoryController: extract RecordInventoryUsageAsync helper; both LogUsage
  (scan page) and LogMaterial (jobs modal, moved from JobsController) call it —
  no more duplicate save/GL logic across two controllers
- Log Material modal: replace radio buttons with prominent toggle buttons so the
  active mode (Amount Used vs Amount Remaining) is always visually obvious; add
  always-visible preview line showing exactly what will be logged before saving
- Edit Usage modal: add quantity field (pre-populated from existing transaction)
  with delta adjustment to InventoryItem.QuantityOnHand on save; include
  completed/terminal jobs in the dropdown so entries can be corrected after a
  job is marked done
- Scan page job picker: include jobs completed within the last 7 days (marked
  with '(completed)') so usage can be logged after a job is finished

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:31:02 -04:00
spouliot f453a95f28 Add hover tooltips on job list rows showing description and PO number
Adds CustomerPO to JobListDto (maps by convention), then builds a
Bootstrap tooltip per row with description · PO: xxx, skipping blank
fields. Rows with neither get no tooltip. Helps identify jobs at a
glance without opening the details page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 18:53:52 -04:00
spouliot d9e98a55d2 Fix customer email inputs to allow comma-separated addresses
type="email" triggers jQuery Validate's email rule which rejects commas,
blocking multi-address input despite the multiple attribute. Switching to
type="text" defers validation to the server-side SplitEmails/MailAddress
logic in the DTO which already handles comma-separated lists correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:16:30 -04:00
spouliot 99deca3b62 Default imported formula templates to active regardless of export state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 11:14:48 -04:00
spouliot 23e64829bb Fix formula export/import: embed fields as real JSON array, not escaped string
Previously FieldsJson was serialized as an escaped string in the export
file, which was fragile and unreadable. Now parsed into a JsonElement and
embedded as a proper JSON array under the key "fields". Import reads it
back with GetRawText() to reconstruct the stored string. This prevents
the null/empty fields bug caused by manually-edited or round-tripped files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 11:06:38 -04:00
spouliot cd4c233b60 Fix formula export casing: use camelCase to match import property lookups
System.Text.Json defaults to PascalCase for anonymous types, producing
"Name"/"OutputMode" etc., while the import used TryGetProperty("name")
causing every template to fail with "no name". Adding CamelCase naming
policy aligns the export format with what the import expects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:52:58 -04:00
spouliot 6c07216c64 Fix custom formula item pricing: multiply by quantity, not divide
ManualUnitPrice holds the per-item formula result. The previous code
incorrectly treated it as the batch total and divided by Quantity,
causing the unit price to shrink as quantity increased. Now follows
the same pattern as every other ManualUnitPrice path in this method.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:27:11 -04:00
spouliot b23bea6db0 Add formula template export/import and unsaved-changes guard
- Export: GET /CompanySettings/ExportCustomItemTemplates downloads all
  company templates as an indented JSON backup (strips internal IDs/paths)
- Import: POST /CompanySettings/ImportCustomItemTemplates restores from
  that file; runs full field + formula validation, skips name duplicates,
  returns per-item results (imported / skipped / errors)
- Unsaved-changes guard: cfModal now intercepts backdrop/ESC/X when the
  form is dirty and prompts before discarding work
- Export and Import buttons added to the Custom Formulas card header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 09:24:02 -04:00
spouliot cf07356147 Fix all NU1605 errors: suppress via Directory.Build.props instead of per-package pins
NCalc2 -> Antlr4 -> Antlr4.Runtime -> NETStandard.Library 1.6.0 triggers 6+
NU1605 downgrade warnings on linux-x64 publish (System.IO.FileSystem.Primitives,
System.Text.Encoding.Extensions, System.Diagnostics.Tracing, Microsoft.Win32.Primitives,
System.IO.FileSystem, System.Net.Primitives). All are harmless — .NET 8 supplies
these natively. Directory.Build.props suppresses NU1605 solution-wide cleanly.
Removes the individual System.Runtime.InteropServices pin added in previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 23:41:56 -04:00
spouliot 39b103a482 Fix NU1605 package downgrade: pin System.Runtime.InteropServices 4.3.0
NCalc2 -> Antlr4 -> NETStandard.Library transitive dependency chain requires
System.Runtime.InteropServices >= 4.3.0, but the resolved version was 4.1.0.
Explicit pin in Application.csproj resolves the Jenkins publish failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 23:27:26 -04:00
spouliot 4aae2df5b5 Merge dev into master
Includes: Community Formula Library, Custom Formula Templates, Employee Timeclock,
Formula Library ratings, Job Profitability report, Quote Revision History,
flat-rate coat wizard UX improvements, customer import dedup fixes, inventory
incoming powder fixes, Custom Powder Order line item, and various bug fixes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 23:16:10 -04:00
spouliot e4a256a6c4 Fix subscription expiry logic and HTML entities in page titles
Subscription expiry (SubscriptionExpiryBackgroundService):
- Trials with no grace period now go directly Active -> Expired instead
  of briefly entering GracePeriod for a day, which was causing repeated
  'Grace Period Started' admin notification emails
- Remove redundant isTrial variable (query already filters to non-Stripe
  companies, so all processed companies are trials by definition)
- Save per-company inside the loop so a single SaveChangesAsync failure
  no longer discards all other companies' status changes and notification
  log entries (which was the other cause of repeated emails)

HTML entities in page titles (33 views):
- Replace &ndash; / &mdash; with plain ' - ' in ViewData["Title"] C#
  strings; Razor HTML-encodes these when rendering @ViewData["Title"],
  causing browsers to display the literal text '&ndash;' instead of a dash

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:58:37 -04:00
spouliot 04d16109ae Simplify location display on inventory QR label
Plain text 'Location: <value>' in larger bold font instead of
pill badge with map pin icon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 18:12:10 -04:00
spouliot f0f3717681 Fix three bugs: vendor duplicate check, page size dropdown, label location
- Vendor Create: reject duplicate company names (case-insensitive) before
  saving; works for both the standalone form and the inline quick-add modal
- _Pagination: define changePageSize() JS function (was called but never
  existed, breaking page size dropdown on every paginated list)
- Inventory Label: show bin/location on printed QR code labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 18:12:07 -04:00
spouliot e23b006139 Add color family filter to inventory index
Adds an 'All Colors' dropdown to the inventory filter bar populated from
the ColorFamilies values already stored on inventory items. Selecting a
family (e.g. 'Red') returns only items tagged with that family.

Also refactors the 16-branch if/else filter builder into a single
composable predicate, making future filter additions trivial.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 18:12:05 -04:00
spouliot 0f35946973 Fix dark mode: main settings nav tab buttons showing white UA background
The #settingsTabs <button> elements had no explicit background-color,
letting browser UA button styling (white) bleed through in dark mode.
Added transparent overrides so the dark body background shows instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 18:12:02 -04:00
spouliot 10f668fd73 Merge dev into master for prod deploy 2026-05-24 10:46:23 -04:00
spouliot b7ab85ff92 Merge dev into master: QR scan URL fixes and http scheme failsafe 2026-05-22 17:41:01 -04:00
spouliot ce7b00b68c Merge dev into master: inventory bin filter, print bin, mobile login fixes, QR scan fix 2026-05-22 15:22:38 -04:00
spouliot c5c1244177 Merge dev into master
- Inline item editing on Job/Quote/Invoice Details pages
- Live pricing summary and Job Costing card updates on save
- PatchItem legacy fallback for jobs without PricingBreakdownJson
- GetCostingBreakdown revenue from FinalPrice (not invoice total)
- Help docs: Inline Price Editing sections added to all three detail pages
- AI knowledge base updated with inline editing and costing revenue behavior
- AGENTS.md tracked; .gitignore updated for Claude Code settings and build logs
- Resolve conflict in Payment/Index.cshtml (em dash entity style)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 08:35:29 -04:00
spouliot 25140554ad Merge hotfixes: Stripe receipt_email, surcharge fix, void deposit/credit, cache headers
- Remove receipt_email from Stripe PaymentIntent (any email accepted at checkout)
- Fix surcharge payment: input/validation based on total-with-fee, not base amount
- Add InvariantCulture to payment JS literals
- Fix voided invoice leaving deposits locked (re-releases for next invoice)
- Convert non-deposit payments to CRED- credits on void (preserves money trail)
- Cache-Control: no-store on authenticated pages (prevents browser cache corruption)
- Fix Edit Payment onclick encoding for apostrophes in reference/notes

Inline item editing (7fa385a) held in dev pending further testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:19:10 -04:00
spouliot 46cadea367 Add Cache-Control: no-store for authenticated pages; fix payment onclick encoding
Prevents browsers from caching authenticated pages, which resolves stale/corrupt
cache bugs (e.g. Firefox refusing to navigate to a specific invoice). Also fixes
the Edit Payment button onclick to use Json.Serialize for Reference/Notes so
apostrophes and other special characters don't break the JavaScript string literal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:18:04 -04:00
spouliot cfe937c0c3 Convert non-deposit payments to customer credits on invoice void
When voiding an invoice that has non-deposit payments (e.g. CC charges),
those payments are now converted to CRED- Deposit records so the money
trail is preserved and the credit auto-applies to the replacement invoice.
Deposits that were applied to the voided invoice are also re-released so
they can auto-apply again. Void confirmation dialog and success message
both reflect the credit amount when applicable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:18:03 -04:00
spouliot 3ad6b0d08f Fix voided invoice leaving deposits locked as applied
When an invoice was voided, deposits auto-applied at invoice creation
kept their AppliedToInvoiceId pointing at the voided invoice. The
replacement invoice lookup (AppliedToInvoiceId == null) skipped them,
so the deposit was never re-applied and the customer was charged in full.

Void now clears AppliedToInvoiceId/AppliedDate on all deposits tied to
the invoice so they're available for the next invoice, and credits the
CustomerDeposits 2300 liability account to restore the balance that was
debited when the deposits were originally applied.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:18:01 -04:00
spouliot fdac0240d1 Fix Stripe receipt_email + online payment surcharge and hardening
Remove receipt_email from PaymentIntent creation so customers can use
any email at Stripe checkout without a stored-email mismatch blocking
payment. Remove now-dead CustomerEmail from PaymentPageViewModel.

Fix surcharge payment input: amount field now represents the total the
customer pays (including fee); JS back-calculates base before sending
to server. Add InvariantCulture to numeric Razor→JS literals to prevent
comma-decimal cultures from truncating surcharge values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:17:57 -04:00
667 changed files with 64896 additions and 200708 deletions
-105
View File
@@ -1,105 +0,0 @@
# Authorization Update Guide for Existing Controllers
## Overview
All existing controllers need to be updated with appropriate authorization policies to work with the multi-tenancy system.
## Required Changes
### 1. Add Authorization Attribute to Controllers
Add the `[Authorize(Policy = "CanViewData")]` attribute to all existing controllers:
- CustomersController
- JobsController
- QuotesController
- InventoryController
- EquipmentController
- MaintenanceController
- ShopFloorController
- ReportsController
- SettingsController
**Example:**
```csharp
[Authorize(Policy = "CanViewData")]
public class CustomersController : Controller
{
// ... controller code
}
```
### 2. Add Policy-Specific Authorization to Actions
For actions that require elevated permissions, add specific policies:
**Create/Edit/Delete Actions:**
```csharp
[Authorize(Policy = "CanManageJobs")]
public async Task<IActionResult> Create()
{
// ... action code
}
```
**Management Actions:**
```csharp
[Authorize(Policy = "CompanyAdminOnly")]
public async Task<IActionResult> AdminPanel()
{
// ... action code
}
```
## Available Policies
1. **SuperAdminOnly** - Platform administrators only
2. **CompanyAdminOnly** - Company administrators (and SuperAdmin)
3. **CanManageJobs** - Users who can manage jobs
4. **CanManageUsers** - Users who can manage other users
5. **CanViewData** - All authenticated users
## Controller-Specific Recommendations
### CustomersController
- Index/Details: `[Authorize(Policy = "CanViewData")]`
- Create/Edit/Delete: `[Authorize(Policy = "CanManageJobs")]` or create `CanManageCustomers` policy
### JobsController
- Index/Details: `[Authorize(Policy = "CanViewData")]`
- Create/Edit/Delete: `[Authorize(Policy = "CanManageJobs")]`
### QuotesController
- Index/Details: `[Authorize(Policy = "CanViewData")]`
- Create: Check `CanCreateQuotes` permission
- Approve: Check `CanApproveQuotes` permission
### InventoryController
- Index/Details: `[Authorize(Policy = "CanViewData")]`
- Create/Edit/Delete: Check `CanManageInventory` permission
### EquipmentController & MaintenanceController
- Index/Details: `[Authorize(Policy = "CanViewData")]`
- Create/Edit/Delete: `[Authorize(Policy = "CanManageJobs")]`
### ReportsController
- All actions: `[Authorize(Policy = "CanViewData")]`
### SettingsController
- All actions: `[Authorize(Policy = "CompanyAdminOnly")]`
## Testing Authorization
After adding authorization, test:
1. **As Viewer**: Should only be able to view, no create/edit/delete buttons
2. **As Worker**: Should be able to edit assigned jobs
3. **As Manager**: Should have full job management
4. **As CompanyAdmin**: Should be able to manage users
5. **As SuperAdmin**: Should see all companies' data
## Notes
- The global query filters in `ApplicationDbContext` handle data isolation automatically
- No code changes needed in methods - filtering happens at the database level
- SuperAdmin can bypass filters using `.IgnoreQueryFilters()` when needed
- Always test cross-company access to ensure data isolation works correctly
-213
View File
@@ -1,213 +0,0 @@
# AutoMapper 16.0.0 Configuration Verification
## ✅ CONFIRMED: API Project Uses AutoMapper 16.0.0
The API project is **already correctly configured** with AutoMapper 16.0.0 without the Extensions package.
## 📦 Package Configuration
### API Project (`PowderCoating.Api.csproj`)
```xml
<PackageReference Include="AutoMapper" Version="16.0.0" />
```
**Status:** ✅ Correct - Using AutoMapper 16.0.0 directly
**NOT using:** ❌ AutoMapper.Extensions.Microsoft.DependencyInjection (as requested)
## 🔧 Dependency Injection Configuration
### API Program.cs (Lines 75-83)
```csharp
// Configure AutoMapper
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile<CustomerProfile>();
mc.AddProfile<JobProfile>();
});
IMapper mapper = mapperConfig.CreateMapper();
builder.Services.AddSingleton(mapper);
builder.Services.AddSingleton<IMapper>(mapper);
```
**Status:** ✅ Correctly configured with manual registration
## 📋 Complete AutoMapper Setup Across All Projects
### Summary Table
| Project | Package | Version | Configuration Method | Status |
|---------|---------|---------|---------------------|--------|
| **PowderCoating.Application** | AutoMapper | 16.0.0 | Profile classes | ✅ |
| **PowderCoating.Web** | AutoMapper | 16.0.0 | Manual DI | ✅ |
| **PowderCoating.Api** | AutoMapper | 16.0.0 | Manual DI | ✅ |
### What's NOT Being Used (As Requested)
❌ AutoMapper.Extensions.Microsoft.DependencyInjection
## 🎯 AutoMapper Profiles
Both profiles are registered in the API:
### 1. CustomerProfile ✅
**Location:** `src/PowderCoating.Application/Mappings/CustomerProfile.cs`
**Mappings:**
- Customer → CustomerDto
- CreateCustomerDto → Customer
- UpdateCustomerDto → Customer
- Customer → CustomerListDto
### 2. JobProfile ✅
**Location:** `src/PowderCoating.Application/Mappings/JobProfile.cs`
**Mappings:**
- Job → JobDto
- CreateJobDto → Job
- UpdateJobDto → Job
- Job → JobListDto
- JobItem → JobItemDto
- CreateJobItemDto → JobItem
- Job → ShopFloorJobDto
## 🧪 Testing AutoMapper in API
### Example API Controller Usage
```csharp
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public CustomersController(IUnitOfWork unitOfWork, IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper; // ✅ IMapper injected successfully
}
[HttpGet]
public async Task<ActionResult<IEnumerable<CustomerListDto>>> GetAll()
{
var customers = await _unitOfWork.Customers.GetAllAsync();
var customerDtos = _mapper.Map<List<CustomerListDto>>(customers);
return Ok(customerDtos); // ✅ Mapping works
}
[HttpGet("{id}")]
public async Task<ActionResult<CustomerDto>> GetById(int id)
{
var customer = await _unitOfWork.Customers.GetByIdAsync(id);
if (customer == null) return NotFound();
var customerDto = _mapper.Map<CustomerDto>(customer);
return Ok(customerDto); // ✅ Mapping works
}
[HttpPost]
public async Task<ActionResult<CustomerDto>> Create(CreateCustomerDto dto)
{
var customer = _mapper.Map<Customer>(dto); // ✅ DTO to Entity
await _unitOfWork.Customers.AddAsync(customer);
await _unitOfWork.SaveChangesAsync();
var customerDto = _mapper.Map<CustomerDto>(customer);
return CreatedAtAction(nameof(GetById), new { id = customer.Id }, customerDto);
}
}
```
## 🔍 Why This Configuration is Better
### Benefits of AutoMapper 16.0.0 Without Extensions:
1. **✅ Explicit Configuration**
- You see exactly which profiles are registered
- No "magic" assembly scanning
- Easier to debug
2. **✅ Better Performance**
- Mapper is created once as singleton
- No runtime assembly scanning overhead
- Predictable initialization
3. **✅ Compile-Time Safety**
- Missing profiles fail at startup
- Clear error messages
- No silent failures
4. **✅ Full Control**
- Configure exactly how you want
- No unexpected behaviors from conventions
- Easy to customize
5. **✅ Cleaner Dependencies**
- Only one AutoMapper package needed
- Smaller dependency tree
- Less potential for version conflicts
## 📊 Verification Checklist
### Package References ✅
- [x] PowderCoating.Application has AutoMapper 16.0.0
- [x] PowderCoating.Web has AutoMapper 16.0.0
- [x] PowderCoating.Api has AutoMapper 16.0.0
- [x] NO projects use AutoMapper.Extensions
### Configuration ✅
- [x] CustomerProfile exists and is complete
- [x] JobProfile exists and is complete
- [x] Both profiles registered in Web Program.cs
- [x] Both profiles registered in API Program.cs
- [x] IMapper interface explicitly registered
- [x] Mapper registered as singleton
### Using Statements ✅
- [x] Web Program.cs imports AutoMapper
- [x] Web Program.cs imports PowderCoating.Application.Mappings
- [x] API Program.cs imports AutoMapper
- [x] API Program.cs imports PowderCoating.Application.Mappings
## 🚀 Ready to Use
The API project is **completely ready** to use AutoMapper 16.0.0:
```bash
# Build the API
cd src/PowderCoating.Api
dotnet build
# Expected: Build succeeded. 0 Warning(s) 0 Error(s)
# Run the API
dotnet run
# Access Swagger
# Navigate to: https://localhost:7002
```
### Test Endpoints (Once Running)
1. **GET /api/customers** - Returns list of customers (mapped to DTOs)
2. **GET /api/customers/{id}** - Returns single customer (mapped to DTO)
3. **POST /api/customers** - Creates customer (DTO → Entity mapping)
4. **GET /api/jobs** - Returns jobs (mapped with related data)
All endpoints will use AutoMapper 16.0.0 for object mapping!
## 📝 Summary
**Current State:**
- ✅ API project uses AutoMapper 16.0.0
- ✅ No Extensions package
- ✅ Manual configuration with explicit profile registration
- ✅ IMapper interface properly registered for DI
- ✅ Both CustomerProfile and JobProfile configured
**No changes needed!** The API project is already set up exactly as requested with AutoMapper 16.0.0.
---
**AutoMapper 16.0.0 is fully configured and ready to use in the API project!** 🎉
-161
View File
@@ -1,161 +0,0 @@
# AutoMapper Configuration Error - FIXED
## 🐛 Issue Found
**Error:** AutoMapper dependency injection not working properly - `IMapper` interface couldn't be resolved.
**Root Cause:** The mapper instance was registered, but the `IMapper` interface wasn't explicitly registered, causing dependency injection failures in controllers.
## ✅ Fix Applied
Updated both `Program.cs` files (Web and API) to properly register the `IMapper` interface.
### Before (Incorrect):
```csharp
// Configure AutoMapper
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile<CustomerProfile>();
mc.AddProfile<JobProfile>();
});
builder.Services.AddSingleton(mapperConfig.CreateMapper());
```
**Problem:** This only registered the concrete `Mapper` type, not the `IMapper` interface that controllers depend on.
### After (Correct):
```csharp
// Configure AutoMapper
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile<CustomerProfile>();
mc.AddProfile<JobProfile>();
});
IMapper mapper = mapperConfig.CreateMapper();
builder.Services.AddSingleton(mapper);
builder.Services.AddSingleton<IMapper>(mapper);
```
**Solution:**
1. Create the mapper instance and store it in a variable
2. Register the instance directly
3. Explicitly register it as `IMapper` interface
This ensures that when controllers request `IMapper` via dependency injection, the service provider can resolve it.
## 📝 Why This Matters
Controllers and services use dependency injection like this:
```csharp
public class CustomersController : Controller
{
private readonly IMapper _mapper; // ← Needs IMapper interface
public CustomersController(IMapper mapper)
{
_mapper = mapper;
}
}
```
Without the explicit `IMapper` registration, the DI container can't resolve this dependency, causing runtime errors:
```
InvalidOperationException: Unable to resolve service for type 'AutoMapper.IMapper'
```
## 🎯 Files Modified
1.`src/PowderCoating.Web/Program.cs` - Lines 52-58
2.`src/PowderCoating.Api/Program.cs` - Lines 76-82
## 🧪 Testing the Fix
### In Controllers:
```csharp
public class CustomersController : Controller
{
private readonly IMapper _mapper;
public CustomersController(IMapper mapper)
{
_mapper = mapper; // ✅ Now works!
}
public async Task<IActionResult> Index()
{
var customers = await _unitOfWork.Customers.GetAllAsync();
var dtos = _mapper.Map<List<CustomerDto>>(customers); // ✅ Works!
return View(dtos);
}
}
```
### In API Controllers:
```csharp
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
private readonly IMapper _mapper;
public CustomersController(IMapper mapper)
{
_mapper = mapper; // ✅ Now works!
}
[HttpGet]
public async Task<ActionResult<List<CustomerDto>>> GetAll()
{
var customers = await _unitOfWork.Customers.GetAllAsync();
return Ok(_mapper.Map<List<CustomerDto>>(customers)); // ✅ Works!
}
}
```
## 💡 Alternative Approach (For Reference)
If you were using `AutoMapper.Extensions.Microsoft.DependencyInjection`, you could do:
```csharp
builder.Services.AddAutoMapper(typeof(CustomerProfile).Assembly);
```
But since we're using AutoMapper 16.0 **without** the Extensions package, we need the manual configuration shown above.
## ✅ Verification
After this fix, you should be able to:
1. ✅ Inject `IMapper` into any controller or service
2. ✅ Use `_mapper.Map<TDestination>(source)` without errors
3. ✅ Run the application without DI resolution errors
## 🚀 Build and Run
```bash
# Clean and rebuild
dotnet clean
dotnet build
# Expected: Build succeeded. 0 Warning(s) 0 Error(s)
# Run the application
cd src/PowderCoating.Web
dotnet run
# Should start without errors
```
## 📋 Summary
**What was wrong:** Mapper instance created but `IMapper` interface not registered
**What we fixed:** Explicitly registered both the instance and the `IMapper` interface
**Result:** Dependency injection now works correctly in all controllers and services
---
**AutoMapper configuration is now correct and ready to use!**
-224
View File
@@ -1,224 +0,0 @@
# AutoMapper 16.0.0 ILoggerFactory Fix - SOLVED!
## ✅ The Correct Solution
You were absolutely right! AutoMapper 16.0.0 requires `ILoggerFactory` as the second parameter to the `MapperConfiguration` constructor.
## 🔧 Correct Configuration (Now Applied)
### Both Web and API Program.cs:
```csharp
// Configure AutoMapper
builder.Services.AddSingleton<IMapper>(sp =>
{
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomerProfile());
cfg.AddProfile(new JobProfile());
}, loggerFactory);
return config.CreateMapper();
});
```
## 📝 Why This is Required
### AutoMapper 16.0.0 Constructor Signature:
```csharp
public MapperConfiguration(
Action<IMapperConfigurationExpression> configure,
ILoggerFactory loggerFactory)
```
**Two parameters required:**
1. `Action<IMapperConfigurationExpression>` - The configuration action
2. `ILoggerFactory` - For AutoMapper's internal logging
### Previous versions (pre-16.0):
```csharp
public MapperConfiguration(Action<IMapperConfigurationExpression> configure)
// Only ONE parameter
```
## 🎯 Key Changes in AutoMapper 16.0.0
1. **Logging Integration** - AutoMapper now integrates with Microsoft.Extensions.Logging
2. **Constructor Change** - `ILoggerFactory` is now required
3. **Better Diagnostics** - Mapping errors are logged through the logging framework
## 💡 How It Works
1. **Service Provider** - We get `ILoggerFactory` from the DI container
2. **Pass to Constructor** - Provide it as the second parameter
3. **AutoMapper Uses It** - AutoMapper logs configuration and mapping issues
4. **Integrated Logging** - All logs go to your application's logging pipeline
## ✅ Benefits
### With ILoggerFactory:
- ✅ AutoMapper logs configuration errors
- ✅ Mapping failures are logged with context
- ✅ Performance diagnostics available
- ✅ Integrates with Serilog (already configured in our project)
### Without ILoggerFactory:
- ❌ Constructor error
- ❌ No logging from AutoMapper
- ❌ Harder to debug mapping issues
## 📊 Complete Configuration Flow
```csharp
builder.Services.AddSingleton<IMapper>(sp =>
{
// 1. Get ILoggerFactory from DI container
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
// 2. Create MapperConfiguration with logging
var config = new MapperConfiguration(cfg =>
{
// 3. Register profiles
cfg.AddProfile(new CustomerProfile());
cfg.AddProfile(new JobProfile());
}, loggerFactory); // ← ILoggerFactory passed here
// 4. Create and return mapper
return config.CreateMapper();
});
```
## 🔍 What Gets Logged
With the logger factory configured, AutoMapper will log:
### Configuration Issues:
```
[AutoMapper] Unmapped members found in Customer -> CustomerDto
[AutoMapper] Missing map from X to Y
```
### Runtime Issues:
```
[AutoMapper] Mapping exception: Cannot convert X to Y
[AutoMapper] Property 'PropertyName' not found on destination type
```
### Performance:
```
[AutoMapper] Configuration validated successfully
[AutoMapper] Mapper created for 2 profiles
```
These logs appear in your Serilog output (console and file).
## 📦 Updated Files
### Web Project
`src/PowderCoating.Web/Program.cs` - Lines 51-61
### API Project
`src/PowderCoating.Api/Program.cs` - Lines 75-85
## 🧪 Testing the Fix
After building successfully, you can verify AutoMapper logging works:
```csharp
[ApiController]
[Route("api/test")]
public class TestController : ControllerBase
{
private readonly IMapper _mapper;
private readonly ILogger<TestController> _logger;
public TestController(IMapper mapper, ILogger<TestController> logger)
{
_mapper = mapper;
_logger = logger;
}
[HttpGet]
public IActionResult Test()
{
try
{
var customer = new Customer { /* ... */ };
var dto = _mapper.Map<CustomerDto>(customer);
return Ok(dto);
}
catch (AutoMapperMappingException ex)
{
// AutoMapper will have already logged this!
_logger.LogError(ex, "Mapping failed");
return BadRequest(ex.Message);
}
}
}
```
## 🎯 Build Status
This should now build successfully:
```bash
dotnet clean
dotnet restore
dotnet build
# Expected Output:
# Build succeeded.
# 0 Warning(s)
# 0 Error(s)
```
## 📋 Complete AutoMapper 16.0.0 Requirements
For a working AutoMapper 16.0.0 configuration, you need:
1.**AutoMapper package** - Version 16.0.0
2.**Microsoft.Extensions.Logging.Abstractions** - Version 10.0.0
3.**Profile instances** - `new CustomerProfile()` not `<CustomerProfile>`
4.**ILoggerFactory parameter** - Second parameter to MapperConfiguration
5.**Service provider factory** - Register using factory pattern with DI
All of these are now configured correctly!
## 🔄 Adding More Profiles
When you add new profiles in the future:
```csharp
builder.Services.AddSingleton<IMapper>(sp =>
{
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomerProfile());
cfg.AddProfile(new JobProfile());
cfg.AddProfile(new InventoryProfile()); // ← Add new profiles here
cfg.AddProfile(new QuoteProfile());
}, loggerFactory); // ← Don't forget the loggerFactory!
return config.CreateMapper();
});
```
## 💡 Key Takeaway
**AutoMapper 16.0.0 Constructor:**
```csharp
new MapperConfiguration(
cfg => { /* config */ },
loggerFactory // ← REQUIRED in v16.0.0
)
```
**NOT:**
```csharp
new MapperConfiguration(cfg => { /* config */ }) // ❌ Missing second parameter
```
---
**Thank you for the research! The `ILoggerFactory` parameter was exactly what was needed. This should now build successfully!** 🎉
-227
View File
@@ -1,227 +0,0 @@
# AutoMapper Configuration - Alternative Approach Applied
## 🔧 Updated Configuration Method
If you're still seeing the `MapperConfiguration` constructor error, I've applied an alternative configuration approach that's more compatible.
## ✅ New Configuration (Both Web & API)
### Previous Approach:
```csharp
var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomerProfile());
cfg.AddProfile(new JobProfile());
});
IMapper mapper = mapperConfig.CreateMapper();
builder.Services.AddSingleton(mapper);
builder.Services.AddSingleton<IMapper>(mapper);
```
### New Approach (Now Applied):
```csharp
builder.Services.AddSingleton<IMapper>(sp =>
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomerProfile());
cfg.AddProfile(new JobProfile());
});
return config.CreateMapper();
});
```
## 💡 Why This Works Better
1. **Service Provider Factory** - Configuration happens inside DI factory
2. **Cleaner Scope** - No intermediate variables in Program.cs
3. **Lazy Loading** - Mapper only created when first requested
4. **Single Registration** - Only registers `IMapper` interface once
## 🔍 Troubleshooting Steps
If you're still getting the constructor error, try these steps:
### Step 1: Clean Everything
```bash
# Clean all build artifacts
dotnet clean
# Clear NuGet cache
dotnet nuget locals all --clear
```
### Step 2: Verify Package Versions
```bash
# Check installed packages
dotnet list package
# Should show:
# AutoMapper 16.0.0 in Application, Web, and API projects
```
### Step 3: Restore Packages
```bash
# Force restore
dotnet restore --force
```
### Step 4: Build
```bash
# Build solution
dotnet build
```
## 📦 Required Package Versions
Make sure these are in your project files:
### PowderCoating.Application.csproj
```xml
<PackageReference Include="AutoMapper" Version="16.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
```
### PowderCoating.Web.csproj
```xml
<PackageReference Include="AutoMapper" Version="16.0.0" />
```
### PowderCoating.Api.csproj
```xml
<PackageReference Include="AutoMapper" Version="16.0.0" />
```
## 🐛 Common Issues & Solutions
### Issue 1: "MapperConfiguration does not contain a constructor..."
**Cause:** NuGet cache has old AutoMapper version
**Solution:**
```bash
dotnet nuget locals all --clear
dotnet restore --force
dotnet build
```
### Issue 2: "Profile not found"
**Cause:** Missing using statement
**Solution:** Ensure this is in Program.cs:
```csharp
using AutoMapper;
using PowderCoating.Application.Mappings;
```
### Issue 3: "Cannot resolve IMapper"
**Cause:** Not registered in DI
**Solution:** Check the configuration is inside `Program.cs` before `var app = builder.Build();`
### Issue 4: "Package downgrade warning"
**Cause:** Logging.Abstractions version mismatch
**Solution:** Update Application project:
```xml
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
```
## 🔄 Alternative: Use AutoMapper.Extensions (If All Else Fails)
If you absolutely cannot get the manual configuration working, you can revert to using the Extensions package:
### Add Package:
```xml
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="16.0.0" />
```
### Configure:
```csharp
builder.Services.AddAutoMapper(typeof(CustomerProfile).Assembly);
```
**However**, the manual configuration should work and is preferred for the reasons stated above.
## ✅ Verification Test
After building successfully, create a simple test controller:
```csharp
[ApiController]
[Route("api/test")]
public class TestController : ControllerBase
{
private readonly IMapper _mapper;
public TestController(IMapper mapper)
{
_mapper = mapper;
}
[HttpGet]
public IActionResult Test()
{
var customer = new Customer
{
Id = 1,
CompanyName = "Test Company",
Email = "test@test.com"
};
var dto = _mapper.Map<CustomerDto>(customer);
return Ok(new {
success = true,
mapped = dto
});
}
}
```
Run the API and navigate to: `https://localhost:7002/api/test`
If it returns the mapped DTO, AutoMapper is working correctly!
## 📋 Final Checklist
- [ ] Cleared NuGet cache
- [ ] Deleted bin/ and obj/ folders
- [ ] Restored packages with --force
- [ ] AutoMapper 16.0.0 in all projects
- [ ] Microsoft.Extensions.Logging.Abstractions 10.0.0 in Application
- [ ] Using statements present in Program.cs
- [ ] Configuration inside service registration (before builder.Build())
- [ ] Both Profile classes exist in Application/Mappings/
- [ ] Build succeeds without errors
## 🎯 Expected Build Output
```bash
dotnet build
# Should output:
Microsoft (R) Build Engine version 17.8.0+...
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
All projects are up-to-date for restore.
PowderCoating.Shared -> bin\Debug\net8.0\PowderCoating.Shared.dll
PowderCoating.Core -> bin\Debug\net8.0\PowderCoating.Core.dll
PowderCoating.Application -> bin\Debug\net8.0\PowderCoating.Application.dll
PowderCoating.Infrastructure -> bin\Debug\net8.0\PowderCoating.Infrastructure.dll
PowderCoating.Web -> bin\Debug\net8.0\PowderCoating.Web.dll
PowderCoating.Api -> bin\Debug\net8.0\PowderCoating.Api.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:XX.XX
```
## 💡 If Still Having Issues
1. **Share the exact error message** - The full error with line numbers
2. **Check your .csproj files** - Ensure package versions match
3. **Verify Profile classes** - Make sure they compile independently
4. **Try a new terminal/VS instance** - Sometimes IDEs cache old assemblies
---
**The new configuration method is now applied and should resolve the constructor error!**
-224
View File
@@ -1,224 +0,0 @@
# AutoMapper 16.0 Update - Build Verification Report
## Changes Made
Updated AutoMapper packages to version 16.0.0 in the following projects:
1. **PowderCoating.Application.csproj**
- AutoMapper: 13.0.1 → 16.0.0
2. **PowderCoating.Web.csproj**
- AutoMapper.Extensions.Microsoft.DependencyInjection: 13.0.1 → 16.0.0
3. **PowderCoating.Api.csproj**
- AutoMapper.Extensions.Microsoft.DependencyInjection: 13.0.1 → 16.0.0
## AutoMapper 16.0 Breaking Changes & Required Updates
### 1. Constructor Injection Changes
AutoMapper 16.0 may have changes in how profiles are registered. No code changes needed as we're using the standard `AddAutoMapper()` extension method.
### 2. Compatibility Check
**Compatible Packages:**
- ✅ .NET 8.0 - Fully compatible
- ✅ Microsoft.Extensions.DependencyInjection - Compatible
- ✅ Entity Framework Core 8.0 - Compatible
**No Breaking Changes Expected** for this project because:
- We use standard AutoMapper features (CreateMap, Map)
- Dependency injection is standard pattern
- No custom resolvers or converters in current code
## Build Status: ✅ EXPECTED TO BUILD SUCCESSFULLY
The project structure uses AutoMapper in a standard way:
### Current Usage Pattern (No Changes Needed):
```csharp
// Program.cs - Already correct
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
// Profile classes will work as-is
public class CustomerProfile : Profile
{
public CustomerProfile()
{
CreateMap<Customer, CustomerDto>();
CreateMap<CreateCustomerDto, Customer>();
}
}
// Controller usage will work as-is
public class CustomerController : Controller
{
private readonly IMapper _mapper;
public CustomerController(IMapper mapper)
{
_mapper = mapper;
}
public IActionResult Index()
{
var dto = _mapper.Map<CustomerDto>(customer);
return View(dto);
}
}
```
## Verification Steps When You Build
1. **Restore Packages:**
```bash
dotnet restore PowderCoatingApp.sln
```
2. **Build Solution:**
```bash
dotnet build PowderCoatingApp.sln
```
3. **Check for Warnings:**
Look for any AutoMapper-related warnings in the build output
## Potential Issues & Solutions
### Issue: Package Restore Fails
**Solution:**
```bash
dotnet nuget locals all --clear
dotnet restore --force
```
### Issue: Version Conflict
**Solution:**
All AutoMapper packages should be the same version. Verify:
```bash
dotnet list package | grep AutoMapper
```
### Issue: Runtime Error - "No maps configured"
**Solution:**
Ensure all DTOs have corresponding Profile classes created. We'll need to create these as we develop features.
## Required Profile Classes (To Be Created)
When you start development, you'll need to create AutoMapper Profile classes:
### Example Profiles to Create:
**CustomerProfile.cs** in `PowderCoating.Application/Mappings/`:
```csharp
using AutoMapper;
using PowderCoating.Core.Entities;
using PowderCoating.Application.DTOs.Customer;
namespace PowderCoating.Application.Mappings;
public class CustomerProfile : Profile
{
public CustomerProfile()
{
CreateMap<Customer, CustomerDto>()
.ForMember(dest => dest.PricingTierName,
opt => opt.MapFrom(src => src.PricingTier != null ? src.PricingTier.TierName : null));
CreateMap<CreateCustomerDto, Customer>();
CreateMap<UpdateCustomerDto, Customer>();
CreateMap<Customer, CustomerListDto>()
.ForMember(dest => dest.ContactName,
opt => opt.MapFrom(src => $"{src.ContactFirstName} {src.ContactLastName}"));
}
}
```
**JobProfile.cs** in `PowderCoating.Application/Mappings/`:
```csharp
using AutoMapper;
using PowderCoating.Core.Entities;
using PowderCoating.Application.DTOs.Job;
namespace PowderCoating.Application.Mappings;
public class JobProfile : Profile
{
public JobProfile()
{
CreateMap<Job, JobDto>()
.ForMember(dest => dest.CustomerName,
opt => opt.MapFrom(src => src.Customer.CompanyName))
.ForMember(dest => dest.AssignedEmployeeName,
opt => opt.MapFrom(src => src.AssignedEmployee != null ? src.AssignedEmployee.FullName : null))
.ForMember(dest => dest.StatusDisplay,
opt => opt.MapFrom(src => src.Status.ToString()))
.ForMember(dest => dest.PriorityDisplay,
opt => opt.MapFrom(src => src.Priority.ToString()));
CreateMap<CreateJobDto, Job>();
CreateMap<UpdateJobDto, Job>();
CreateMap<Job, JobListDto>()
.ForMember(dest => dest.CustomerName,
opt => opt.MapFrom(src => src.Customer.CompanyName))
.ForMember(dest => dest.AssignedEmployeeName,
opt => opt.MapFrom(src => src.AssignedEmployee != null ? src.AssignedEmployee.FullName : null))
.ForMember(dest => dest.StatusDisplay,
opt => opt.MapFrom(src => src.Status.ToString()));
CreateMap<JobItem, JobItemDto>();
CreateMap<CreateJobItemDto, JobItem>();
CreateMap<Job, ShopFloorJobDto>()
.ForMember(dest => dest.CustomerName,
opt => opt.MapFrom(src => src.Customer.CompanyName))
.ForMember(dest => dest.AssignedEmployeeName,
opt => opt.MapFrom(src => src.AssignedEmployee != null ? src.AssignedEmployee.FullName : null))
.ForMember(dest => dest.StatusDisplay,
opt => opt.MapFrom(src => src.Status.ToString()))
.ForMember(dest => dest.ItemCount,
opt => opt.MapFrom(src => src.JobItems.Count))
.ForMember(dest => dest.PriorityColor,
opt => opt.MapFrom(src => GetPriorityColor(src.Priority)));
}
private static string GetPriorityColor(JobPriority priority)
{
return priority switch
{
JobPriority.Rush => "red",
JobPriority.Urgent => "orange",
JobPriority.High => "yellow",
JobPriority.Normal => "blue",
JobPriority.Low => "gray",
_ => "blue"
};
}
}
```
## Next Steps After Verifying Build
1. ✅ Update AutoMapper to 16.0 (DONE)
2. ⏭️ Run `dotnet restore` in your environment
3. ⏭️ Run `dotnet build` to verify no errors
4. ⏭️ Create AutoMapper Profile classes as shown above
5. ⏭️ Test the application
## AutoMapper 16.0 New Features You Can Use
AutoMapper 16.0 includes:
- Improved performance
- Better source generator support
- Enhanced null handling
- Better async support
You can leverage these features in your profiles as you develop.
## Conclusion
**Update Complete** - AutoMapper packages updated to version 16.0.0
**Build Expected** - No breaking changes for our usage pattern
⚠️ **Action Required** - Create AutoMapper Profile classes when starting development
The project should build successfully. When you're ready to run the application, you'll need to create the AutoMapper Profile classes as shown in the examples above.
-216
View File
@@ -1,216 +0,0 @@
# Build Errors Fixed
## ✅ Critical Build Error Fixed
### Issue: Naming Conflict in ApplicationDbContext
**Error:**
```
The name 'SeedData' conflicts with the imported type 'PowderCoating.Infrastructure.Data.SeedData'
```
**Location:** `src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs`
**Problem:**
The DbContext had a private method named `SeedData(ModelBuilder modelBuilder)` which conflicted with the static class `SeedData` imported for seeding users and roles.
**Fix Applied:**
Renamed the method from `SeedData` to `SeedInitialData`:
```csharp
// Before (Line 59 & 144)
SeedData(modelBuilder);
private void SeedData(ModelBuilder modelBuilder)
// After
SeedInitialData(modelBuilder);
private void SeedInitialData(ModelBuilder modelBuilder)
```
This resolves the naming conflict while maintaining the same functionality.
## 🔍 Verification Checklist
After this fix, the following should build successfully:
### ✅ Project Structure
```
PowderCoatingApp/
├── src/
│ ├── PowderCoating.Core/ ✅
│ ├── PowderCoating.Application/ ✅
│ ├── PowderCoating.Infrastructure/ ✅ (Now references Shared)
│ ├── PowderCoating.Web/ ✅
│ ├── PowderCoating.Api/ ✅
│ └── PowderCoating.Shared/ ✅
└── tests/
├── PowderCoating.UnitTests/ ✅
└── PowderCoating.IntegrationTests/ ✅
```
### ✅ Key Files Verified
1. **ApplicationDbContext.cs**
-`SeedInitialData` method renamed (no conflict)
- ✅ All DbSet properties defined
- ✅ Relationships configured
- ✅ Soft delete query filters applied
2. **SeedData.cs**
- ✅ References `PowderCoating.Shared.Constants` (Infrastructure now references Shared)
- ✅ Uses `AppConstants.Roles.*` correctly
- ✅ No naming conflicts
3. **ApplicationUser.cs**
- ✅ All properties defined correctly
- ✅ Relationships configured
- ✅ FullName helper property
4. **Customer.cs**
- ✅ Duplicate `Notes` field fixed
- ✅ Collection renamed to `CustomerNotes`
- ✅ String field renamed to `GeneralNotes`
5. **AutoMapper Profiles**
- ✅ CustomerProfile created
- ✅ JobProfile created
- ✅ Both registered in Program.cs files
6. **Program.cs Files**
- ✅ Web: AutoMapper manually configured
- ✅ API: AutoMapper manually configured
- ✅ Both reference `PowderCoating.Application.Mappings`
## 🎯 Build Command Sequence
To verify the build works:
```bash
# Step 1: Clean solution
dotnet clean
# Step 2: Restore packages
dotnet restore
# Step 3: Build solution
dotnet build
# Expected Output:
# Build succeeded.
# 0 Warning(s)
# 0 Error(s)
```
## 📦 All Package References Verified
### Core Project
- ✅ Microsoft.Extensions.Identity.Stores 8.0.11
### Application Project
- ✅ AutoMapper 16.0.0
- ✅ FluentValidation 11.11.0
- ✅ FluentValidation.DependencyInjectionExtensions 11.11.0
- ✅ Microsoft.Extensions.Logging.Abstractions 8.0.2
- ✅ Microsoft.SemanticKernel 1.31.0
- ✅ Microsoft.ML 3.0.1
### Infrastructure Project
- ✅ Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.11
- ✅ Microsoft.EntityFrameworkCore 8.0.11
- ✅ Microsoft.EntityFrameworkCore.SqlServer 8.0.11
- ✅ Microsoft.EntityFrameworkCore.Tools 8.0.11
- ✅ Microsoft.EntityFrameworkCore.Design 8.0.11
-**References Shared project**
### Web Project
- ✅ AutoMapper 16.0.0
- ✅ Microsoft.AspNetCore.Identity.UI 8.0.11
- ✅ Microsoft.EntityFrameworkCore.Design 8.0.11
- ✅ Microsoft.VisualStudio.Web.CodeGeneration.Design 8.0.7
- ✅ Serilog.AspNetCore 8.0.3
- ✅ Serilog.Sinks.File 6.0.0
### API Project
- ✅ AutoMapper 16.0.0
- ✅ Microsoft.AspNetCore.Authentication.JwtBearer 8.0.11
- ✅ Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.11
- ✅ Microsoft.EntityFrameworkCore.Design 8.0.11
- ✅ Swashbuckle.AspNetCore 7.2.0
- ✅ Serilog.AspNetCore 8.0.3
## 🔧 All Project References Verified
### Infrastructure References:
```xml
<ProjectReference Include="..\PowderCoating.Core\PowderCoating.Core.csproj" />
<ProjectReference Include="..\PowderCoating.Application\PowderCoating.Application.csproj" />
<ProjectReference Include="..\PowderCoating.Shared\PowderCoating.Shared.csproj" />
```
### Web References:
```xml
<ProjectReference Include="..\PowderCoating.Core\PowderCoating.Core.csproj" />
<ProjectReference Include="..\PowderCoating.Application\PowderCoating.Application.csproj" />
<ProjectReference Include="..\PowderCoating.Infrastructure\PowderCoating.Infrastructure.csproj" />
```
### API References:
```xml
<ProjectReference Include="..\PowderCoating.Core\PowderCoating.Core.csproj" />
<ProjectReference Include="..\PowderCoating.Application\PowderCoating.Application.csproj" />
<ProjectReference Include="..\PowderCoating.Infrastructure\PowderCoating.Infrastructure.csproj" />
<ProjectReference Include="..\PowderCoating.Shared\PowderCoating.Shared.csproj" />
```
## 🎉 Summary of All Fixes
1.**Naming Conflict** - `SeedData` method renamed to `SeedInitialData`
2.**Missing Reference** - Infrastructure now references Shared
3.**Duplicate Field** - Customer.Notes fixed (CustomerNotes + GeneralNotes)
4.**AutoMapper** - Configured without Extensions package
5.**Packages** - All updated to latest stable versions
6.**Connection String** - Set to SQL Express
## 🚀 Next Steps
The project should now build without errors. To get started:
```bash
# 1. Extract the archive
# 2. Navigate to the solution directory
cd PowderCoatingApp
# 3. Restore packages
dotnet restore
# 4. Build the solution
dotnet build
# 5. Create the database
cd src/PowderCoating.Web
dotnet ef database update --project ../PowderCoating.Infrastructure
# 6. Run the application
dotnet run
# 7. Open browser to https://localhost:7001
# 8. Login with: admin@powdercoating.com / Admin123!
```
## 📝 Common Build Issues (If Any Remain)
### If you see "Type or namespace not found":
1. Ensure all packages are restored: `dotnet restore --force`
2. Clean and rebuild: `dotnet clean && dotnet build`
### If you see "Cannot find DbSet":
1. Check that all entity classes are in `PowderCoating.Core.Entities`
2. Verify using statements in ApplicationDbContext.cs
### If AutoMapper throws errors:
1. Verify both Profile classes exist in `PowderCoating.Application/Mappings/`
2. Check that both are registered in Program.cs files
---
**All build errors have been identified and fixed!**
-280
View File
@@ -1,280 +0,0 @@
# Final Fixes Applied - Build Errors & Package Updates
## ✅ Issues Fixed
### 1. Infrastructure Project Missing Shared Reference
**Problem:** The Infrastructure project didn't reference the Shared project, causing build errors when trying to use constants or shared utilities.
**Solution:** Added project reference to Shared in Infrastructure.csproj:
```xml
<ProjectReference Include="..\PowderCoating.Shared\PowderCoating.Shared.csproj" />
```
### 2. SQL Server Connection String Updated
**Problem:** Connection string was set for LocalDB which may not be installed.
**Solution:** Updated both Web and API appsettings.json to use SQL Server Express:
**Before:**
```json
"Server=(localdb)\\mssqllocaldb;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true"
```
**After:**
```json
"Server=.\\SQLEXPRESS;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true;TrustServerCertificate=true"
```
**Note:** Added `TrustServerCertificate=true` for development to avoid SSL certificate issues.
### 3. All NuGet Packages Updated to Latest Stable Versions
Updated all packages across all projects to the latest compatible versions:
#### Core Packages (All Projects)
| Package | Old Version | New Version |
|---------|-------------|-------------|
| Microsoft.EntityFrameworkCore | 8.0.0 | **8.0.11** |
| Microsoft.EntityFrameworkCore.SqlServer | 8.0.0 | **8.0.11** |
| Microsoft.EntityFrameworkCore.Design | 8.0.0 | **8.0.11** |
| Microsoft.EntityFrameworkCore.Tools | 8.0.0 | **8.0.11** |
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 8.0.0 | **8.0.11** |
| Microsoft.AspNetCore.Identity.UI | 8.0.0 | **8.0.11** |
| Microsoft.AspNetCore.Authentication.JwtBearer | 8.0.0 | **8.0.11** |
#### Application Packages
| Package | Old Version | New Version |
|---------|-------------|-------------|
| FluentValidation | 11.9.0 | **11.11.0** |
| FluentValidation.DependencyInjectionExtensions | 11.9.0 | **11.11.0** |
| Microsoft.Extensions.Logging.Abstractions | 8.0.0 | **8.0.2** |
| Microsoft.SemanticKernel | 1.0.1 | **1.31.0** |
#### Web/API Packages
| Package | Old Version | New Version |
|---------|-------------|-------------|
| Serilog.AspNetCore | 8.0.0 | **8.0.3** |
| Serilog.Sinks.File | 5.0.0 | **6.0.0** |
| Swashbuckle.AspNetCore | 6.5.0 | **7.2.0** |
| Microsoft.VisualStudio.Web.CodeGeneration.Design | 8.0.0 | **8.0.7** |
#### Test Packages
| Package | Old Version | New Version |
|---------|-------------|-------------|
| Microsoft.NET.Test.Sdk | 17.8.0 | **17.12.0** |
| Moq | 4.20.70 | **4.20.72** |
| xunit | 2.6.2 | **2.9.2** |
| xunit.runner.visualstudio | 2.5.4 | **2.8.2** |
| coverlet.collector | 6.0.0 | **6.0.2** |
| Microsoft.AspNetCore.Mvc.Testing | 8.0.0 | **8.0.11** |
| Microsoft.EntityFrameworkCore.InMemory | 8.0.0 | **8.0.11** |
#### Unchanged (Already Latest)
| Package | Version |
|---------|---------|
| AutoMapper | **16.0.0** ✅ |
| Microsoft.ML | **3.0.1** ✅ |
## 📦 Project References Updated
### Infrastructure Project Now References:
1. PowderCoating.Core ✅
2. PowderCoating.Application ✅
3. **PowderCoating.Shared** ✅ (NEWLY ADDED)
This allows Infrastructure to access:
- `AppConstants` from Shared
- `CacheKeys` and other shared utilities
- Common enums and helpers
## 🗄️ Database Connection Options
The project is now configured for **SQL Server Express** by default.
### SQL Server Express (Default - Recommended)
```json
"Server=.\\SQLEXPRESS;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true;TrustServerCertificate=true"
```
### Alternative Connection Strings:
#### LocalDB (if you prefer)
```json
"Server=(localdb)\\mssqllocaldb;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true"
```
#### Full SQL Server
```json
"Server=YOUR_SERVER_NAME;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true;TrustServerCertificate=true"
```
#### SQL Server with Authentication
```json
"Server=YOUR_SERVER;Database=PowderCoatingDb;User Id=sa;Password=YourPassword;MultipleActiveResultSets=true;TrustServerCertificate=true"
```
#### Azure SQL
```json
"Server=tcp:yourserver.database.windows.net,1433;Database=PowderCoatingDb;User Id=yourusername;Password=yourpassword;Encrypt=true;MultipleActiveResultSets=true"
```
## 🔧 Build Verification
### Before These Fixes:
```
Build FAILED
- Infrastructure couldn't find Shared types
- Potential version conflicts
```
### After These Fixes:
```bash
dotnet restore
dotnet build
```
**Expected Output:**
```
Build succeeded.
0 Warning(s)
0 Error(s)
```
## 📋 Files Modified
### Project Files (.csproj):
1.`src/PowderCoating.Core/PowderCoating.Core.csproj`
2.`src/PowderCoating.Application/PowderCoating.Application.csproj`
3.`src/PowderCoating.Infrastructure/PowderCoating.Infrastructure.csproj` (+ Shared reference)
4.`src/PowderCoating.Web/PowderCoating.Web.csproj`
5.`src/PowderCoating.Api/PowderCoating.Api.csproj`
6.`tests/PowderCoating.UnitTests/PowderCoating.UnitTests.csproj`
7.`tests/PowderCoating.IntegrationTests/PowderCoating.IntegrationTests.csproj`
### Configuration Files:
1.`src/PowderCoating.Web/appsettings.json` (SQL Express connection)
2.`src/PowderCoating.Api/appsettings.json` (SQL Express connection)
## 🎯 Why These Updates Matter
### Security & Bug Fixes
- ✅ Latest EF Core 8.0.11 includes security patches
- ✅ Identity framework security improvements
- ✅ Fixed known vulnerabilities in dependencies
### Performance Improvements
- ✅ EF Core 8.0.11 has query optimization improvements
- ✅ Serilog updates improve logging performance
- ✅ Semantic Kernel 1.31.0 has significant AI performance improvements
### New Features
- ✅ FluentValidation 11.11.0 adds new validation rules
- ✅ Swashbuckle 7.2.0 improves Swagger UI
- ✅ xUnit 2.9.2 adds better test reporting
### Stability
- ✅ All packages tested together for .NET 8.0
- ✅ No version conflicts
- ✅ Production-ready versions
## 🚀 Getting Started with SQL Express
### Step 1: Verify SQL Express is Installed
```powershell
# Check if SQL Express is running
Get-Service | Where-Object {$_.Name -like "*SQL*"}
```
### Step 2: Start SQL Express (if not running)
```powershell
# Start SQL Express
Start-Service MSSQL$SQLEXPRESS
```
### Step 3: Create Database
```bash
cd src/PowderCoating.Web
dotnet ef database update --project ../PowderCoating.Infrastructure
```
**Expected Output:**
```
Applying migration '20250204_InitialCreate'.
Done.
```
### Step 4: Run the Application
```bash
dotnet run
```
## 🐛 Troubleshooting
### Error: "A network-related or instance-specific error occurred"
**Solutions:**
1. Verify SQL Express is running
2. Check instance name is correct (`.\\SQLEXPRESS`)
3. Enable TCP/IP in SQL Server Configuration Manager
4. Try different connection string (see options above)
### Error: "Login failed for user"
**Solutions:**
1. Use Windows Authentication (Trusted_Connection=true)
2. Or use SQL Authentication with correct username/password
3. Ensure user has permissions on database
### Error: "Cannot open database"
**Solution:**
```bash
dotnet ef database update --project src/PowderCoating.Infrastructure --startup-project src/PowderCoating.Web
```
### Package Restore Issues
```bash
# Clear caches and restore
dotnet nuget locals all --clear
dotnet restore --force
dotnet build
```
## ✅ Verification Checklist
After downloading and extracting:
- [ ] Run `dotnet restore` - should succeed
- [ ] Run `dotnet build` - should succeed with 0 errors
- [ ] Verify SQL Express is installed and running
- [ ] Run `dotnet ef database update` - should create database
- [ ] Run `dotnet run` in Web project - should start successfully
- [ ] Navigate to https://localhost:7001 - should see login page
- [ ] Login with admin@powdercoating.com / Admin123!
## 📊 Package Version Summary
**Total Packages Updated:** 23
**Total Projects Modified:** 7
**New Project References Added:** 1 (Infrastructure → Shared)
**Connection Strings Updated:** 2
## 🎉 What You Get
**Build-ready** - No compilation errors
**Latest packages** - All security patches and improvements
**SQL Express ready** - Pre-configured connection string
**Proper references** - All project dependencies resolved
**Production quality** - Stable, tested package versions
## 📝 Next Steps
1. **Extract the archive**
2. **Verify SQL Express is running**
3. **Run `dotnet restore`**
4. **Run `dotnet build`** - Should succeed!
5. **Create database:** `dotnet ef database update`
6. **Run the application:** `dotnet run`
7. **Login** with default admin credentials
8. **Start building** your powder coating management features!
---
**All fixes applied and tested. Project is ready to build and run!**
-19
View File
@@ -1,19 +0,0 @@
We just want some coaters to use it and see where we have a hit, and where we have a miss. We still haven't done any instructional type videos at all since we're still changing things up a bit so most of it will be pretty self explanatory, but some things might not be! lol
You sent
Most of it will work, but some things might crash!
------------------------------------------------------------
All we ask is 3 things.
1. Give us some feedback! We want the good, the bad, and the ugly. No need to spare our feelings. If it sucks, tell us. If it F'ing rocks....tell us.
2. Do not share any information or screenshots of the app with anyone at this point. We have too many people in this industry that like to play copycat and we're trying to keep their eyes off of the application for as long as humanly possible.
3. Data may disappear on the site. Chances of that happening are slim right now since most database changes have been made, but if something goes BOOM....data may disappear. There is a CSV export feature in the Tools section. If you do a bunch of data entry....use the export to give yourself a backup 🙂
=====================================
Here is the URL: http://appdev.scppowdercoating.com:8080/
Click on "Start your 7 day free trial" and it will prompt you to create your company and an initial user login.
Then you'll get into the app 🙂
+118 -475
View File
@@ -1,59 +1,41 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Guidance for Claude Code when working in this repository.
## Project Overview
A production-ready ASP.NET Core 8.0 MVC application for managing powder coating business operations. The application implements Clean Architecture with six projects across three layers (Domain, Application, Infrastructure) plus two presentation layers (Web MVC, RESTful API).
ASP.NET Core 8.0 MVC application for powder coating business operations. Clean Architecture:
**Core** (entities/interfaces) → **Application** (DTOs/profiles) → **Infrastructure** (EF/repos/services) + **Web** (Razor MVC) + **Api** (REST/JWT).
## Essential Commands
### Building and Running
```bash
# Build entire solution
# Build
dotnet build
# Run web application (MVC)
cd src/PowderCoating.Web
dotnet run
# Access at: https://localhost:58461
# Web MVC — https://localhost:58461
cd src/PowderCoating.Web && dotnet run
# Run web with auto-reload
dotnet watch run
# API — Swagger at root URL
cd src/PowderCoating.Api && dotnet run
# Run API
cd src/PowderCoating.Api
dotnet run
# Swagger UI at root URL
# Run tests
dotnet test # All tests
dotnet test tests/PowderCoating.UnitTests # Unit tests only
dotnet test tests/PowderCoating.IntegrationTests # Integration tests only
# Tests
dotnet test
dotnet test tests/PowderCoating.UnitTests
dotnet test tests/PowderCoating.IntegrationTests
```
### Database Operations
### Database (EF Core)
Run from `src/PowderCoating.Web`. **Always include `--context ApplicationDbContext`** — multiple DbContexts exist; omitting it throws.
```bash
# All EF commands run from Web project directory
cd src/PowderCoating.Web
# Create migration (must specify Infrastructure project)
dotnet ef migrations add MigrationName --project ../PowderCoating.Infrastructure
# Apply migrations
dotnet ef database update --project ../PowderCoating.Infrastructure
# Reset database (WARNING: deletes all data)
dotnet ef database drop --project ../PowderCoating.Infrastructure
dotnet ef database update --project ../PowderCoating.Infrastructure
# List migrations
dotnet ef migrations list --project ../PowderCoating.Infrastructure
# Remove last migration (if not applied)
dotnet ef migrations remove --project ../PowderCoating.Infrastructure
dotnet ef migrations add <Name> --project ../PowderCoating.Infrastructure --context ApplicationDbContext
dotnet ef database update --project ../PowderCoating.Infrastructure --context ApplicationDbContext
dotnet ef migrations remove --project ../PowderCoating.Infrastructure --context ApplicationDbContext
dotnet ef migrations list --project ../PowderCoating.Infrastructure --context ApplicationDbContext
dotnet ef database drop --project ../PowderCoating.Infrastructure --context ApplicationDbContext
```
### Default Credentials
@@ -65,70 +47,26 @@ SuperAdmin (seed): spouliot@powdercoatinglogix.com / SuperAdmin123!
Company Admin (seed): demo@powdercoatinglogix.com / CompanyAdmin123!
```
## Architecture Overview
## Architecture
### Clean Architecture Layers
### Layers
- **Core** — Entities, enums, repository + service interfaces. `BaseEntity` provides `Id`, `CompanyId`, `CreatedAt`, `UpdatedAt`, `IsDeleted`, audit fields on every entity.
- **Application** — DTOs, AutoMapper profiles (auto-discovered via `cfg.AddMaps()`; `PricingTierProfile` is an exception — registered manually in `Program.cs`), service interfaces. No UI/infra deps.
- **Infrastructure** — `ApplicationDbContext`, `Repository<T>`, `UnitOfWork`. Seed data is **manual only** via Platform Management → Seed Data.
- **Web** — Razor MVC + Bootstrap 5. **Api** — JWT Bearer, Swagger.
**Domain Layer (PowderCoating.Core)**
- Contains business entities, enums, and repository interfaces
- `BaseEntity` provides common properties for all entities (Id, CompanyId, CreatedAt, UpdatedAt, IsDeleted, audit fields)
- All entities inherit from BaseEntity and support soft delete
- No dependencies on other projects
### Global Query Filters (always active)
- Soft deletes: `IsDeleted == false`
- Multi-tenancy: non-SuperAdmin sees only their `CompanyId`
- Bypass: `ignoreQueryFilters: true` on repository methods
**Application Layer (PowderCoating.Application)**
- DTOs organized by domain (Customer, Job, Equipment, Inventory, Maintenance)
- AutoMapper profiles with reverse mappings
- Service interfaces (IFileService, etc.)
- No UI or infrastructure dependencies
**Infrastructure Layer (PowderCoating.Infrastructure)**
- `ApplicationDbContext` with global query filters for soft deletes and multi-tenancy
- Generic `Repository<T>` implementing `IRepository<T>`
- `UnitOfWork` implementing `IUnitOfWork` with lazy-loaded repositories
- Seed data is triggered **manually** via Platform Management → Seed Data (not automatic on startup)
**Presentation Layers**
- `PowderCoating.Web`: MVC application with Razor views, Bootstrap 5 UI
- `PowderCoating.Api`: RESTful API with JWT authentication, Swagger documentation
### Key Design Patterns
**Repository Pattern**
- Generic `Repository<T>` in Infrastructure
- All CRUD operations, search, pagination, eager loading support
- Soft delete with `SoftDeleteAsync()` method
**Unit of Work Pattern**
- Coordinates multiple repositories
- Transaction support: `BeginTransactionAsync()`, `CommitTransactionAsync()`, `RollbackTransactionAsync()`
- Lazy instantiation of repositories
- `SaveChangesAsync()` or `CompleteAsync()` to persist changes
**Dependency Injection**
- All dependencies registered in `Program.cs`
- Controllers inject `IUnitOfWork` and `IMapper`
- Services are scoped to request lifetime
**Global Query Filters**
- Soft deletes: All queries automatically filter `IsDeleted == false`
- Multi-tenancy: Non-SuperAdmin users see only their company data
- Bypass with `ignoreQueryFilters: true` parameter in repository methods
### Multi-Tenancy Implementation
- `CompanyId` foreign key on all business entities
- `ITenantContext` injected into DbContext resolves current company
- SuperAdmin role can view all companies
- Global query filters enforce company isolation at database level
- Users have both system role (SuperAdmin) and company role (CompanyAdmin, Manager, Worker, Viewer)
**Critical:** global filters are not sufficient on their own. Every `FindAsync`/`GetAllAsync` in a controller must also include an explicit `CompanyId == currentCompanyId` predicate — defense in depth.
## Data Access Rules (ENFORCE THESE)
> **`ApplicationDbContext` is NEVER injected into a controller.**
> All data access in controllers goes through `IUnitOfWork`. No exceptions outside the list below.
> **This rule is enforced at startup:** `EnforceDataAccessArchitecture()` in `Program.cs` scans all
> controllers at boot and throws if any non-exempt controller injects `ApplicationDbContext`.
> Full rationale and permanent exceptions list: `docs/DATA_ACCESS_ARCHITECTURE.md`
> All data access goes through `IUnitOfWork`. Enforced at startup by `EnforceDataAccessArchitecture()` in `Program.cs`.
> Full rationale + permanent exceptions: `docs/DATA_ACCESS_ARCHITECTURE.md`
### Three tiers — use the right one:
@@ -141,346 +79,57 @@ await _unitOfWork.CompleteAsync();
**Tier 2 — Complex domain queries** → typed repositories on `IUnitOfWork`
```csharp
// Include chains and domain-specific queries belong in the repository, not the controller
var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id);
var invoice = await _unitOfWork.Invoices.LoadForViewAsync(id);
var quote = await _unitOfWork.Quotes.GetByApprovalTokenAsync(token);
```
Typed repositories: `IJobRepository`, `IInvoiceRepository`, `IQuoteRepository`,
`ICustomerRepository`, `IBillRepository`, `IPurchaseOrderRepository`
— defined in `Core/Interfaces/Repositories/`, implemented in `Infrastructure/Repositories/`
Typed repos: `IJobRepository`, `IInvoiceRepository`, `IQuoteRepository`, `ICustomerRepository`, `IBillRepository`, `IPurchaseOrderRepository`
— defined in `Core/Interfaces/Repositories/`, implemented in `Infrastructure/Repositories/`.
**Tier 3 — Aggregate/reporting queries** → injected read services
**Tier 3 — Aggregate/reporting** → injected read services
```csharp
// P&L, AR aging, cycle time, powder usage — shaped DTOs, never tracked entities
var aging = await _financialReports.GetArAgingAsync(companyId);
```
Services: `IFinancialReportService`, `IOperationalReportService`
— defined in `Core/Interfaces/Services/`, implemented in `Infrastructure/Services/`
Services: `IFinancialReportService`, `IOperationalReportService`.
### Permanent exceptions (ApplicationDbContext allowed — intentional, documented):
### Permanent exceptions (ApplicationDbContext allowed — intentional):
`StripeWebhookController`, `WebhooksController`, `PaymentController`, `RegistrationController`,
`DataExportController`, `AccountDataExportController`, `DataPurgeController`,
`SystemInfoController`, `SystemLogsController`, `CompanyHealthController`
If you think you need a new exception, you almost certainly don't. Check the spec first.
---
## Domain Concepts
### Job Lifecycle
16 statuses in `JobStatusLookup` **table — NOT an enum**: Pending → Quoted → Approved → InPreparation → Sandblasting → MaskingTaping → Cleaning → InOven → Coating → Curing → QualityCheck → Completed → ReadyForPickup → Delivered | OnHold | Cancelled.
Use `.Include(j => j.JobStatus)` and filter on `!j.JobStatus.IsTerminalStatus`.
**Priorities**: Low, Normal, High, Urgent, Rush (color-coded in UI).
### Customers
- **Commercial**: B2B, pricing tiers, credit limits
- **Non-Commercial**: individual/residential
### Inventory
Transactions tracked in `InventoryTransaction` (Purchase, Sale, Adjustment, Transfer, Return, Waste, Initial). Reorder points trigger alerts.
### Equipment & Maintenance
Equipment: Operational, NeedsMaintenance, UnderMaintenance, OutOfService, Retired.
Maintenance priority: Low/Normal/High/Critical. Status: Scheduled/InProgress/Completed/Cancelled/Overdue.
---
## Data Access Patterns
## Pricing
### Common Controller Pattern
```csharp
public class ExampleController : Controller
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public ExampleController(IUnitOfWork unitOfWork, IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<IActionResult> Index()
{
var entities = await _unitOfWork.Examples.GetAllAsync();
var dtos = _mapper.Map<List<ExampleDto>>(entities);
return View(dtos);
}
[HttpPost]
public async Task<IActionResult> Create(CreateExampleDto dto)
{
var entity = _mapper.Map<Example>(dto);
await _unitOfWork.Examples.AddAsync(entity);
await _unitOfWork.CompleteAsync();
return RedirectToAction(nameof(Index));
}
public async Task<IActionResult> Delete(int id)
{
await _unitOfWork.Examples.SoftDeleteAsync(id);
await _unitOfWork.CompleteAsync();
return RedirectToAction(nameof(Index));
}
}
```
### Using Unit of Work Repositories
All entity repositories are available via `IUnitOfWork` properties:
- `_unitOfWork.Customers`
- `_unitOfWork.Jobs`
- `_unitOfWork.JobItems`
- `_unitOfWork.Quotes`
- `_unitOfWork.InventoryItems`
- `_unitOfWork.Equipment`
- `_unitOfWork.MaintenanceRecords`
- Plus additional entities (Suppliers, JobPhotos, JobNotes, etc.)
### Eager Loading Related Data
```csharp
// Load customer with related data
var customer = await _unitOfWork.Customers.GetByIdAsync(
id,
c => c.Jobs,
c => c.Quotes,
c => c.PricingTier
);
// Find with predicate and includes
var activeJobs = await _unitOfWork.Jobs.FindAsync(
j => j.Status != JobStatus.Completed,
j => j.Customer,
j => j.JobItems
);
```
### Pagination
```csharp
var pagedJobs = await _unitOfWork.Jobs.GetPagedAsync(
pageNumber: 1,
pageSize: 25,
j => j.Status == JobStatus.InPreparation,
j => j.Customer
);
```
## Important Domain Concepts
### Job Lifecycle
Jobs progress through 16 statuses:
1. **Pending** → Initial state
2. **Quoted** → Quote generated
3. **Approved** → Customer approved
4. **InPreparation** → Job prep started
5. **Sandblasting** → Surface prep
6. **MaskingTaping** → Masking areas
7. **Cleaning** → Pre-coat cleaning
8. **InOven** → Pre-heating
9. **Coating** → Applying powder
10. **Curing** → Heat curing
11. **QualityCheck** → Inspection
12. **Completed** → Work finished
13. **ReadyForPickup** → Awaiting customer
14. **Delivered** → Job delivered
15. **OnHold** → Paused
16. **Cancelled** → Cancelled
**Job Priorities**: Low, Normal, High, Urgent, Rush (color-coded in UI)
### Customer Types
- **Commercial**: B2B customers with pricing tiers, credit limits
- **Non-Commercial**: Individual customers, typically simpler pricing
### Inventory Management
**Transaction Types**: Purchase, Sale, Adjustment, Transfer, Return, Waste, Initial
- All transactions tracked in `InventoryTransaction` entity
- Reorder points trigger low-stock alerts
### Equipment & Maintenance
**Equipment Status**: Operational, NeedsMaintenance, UnderMaintenance, OutOfService, Retired
**Maintenance Priority**: Low, Normal, High, Critical
**Maintenance Status**: Scheduled, InProgress, Completed, Cancelled, Overdue
## Configuration Files
### Web Application (src/PowderCoating.Web/appsettings.json)
```json
{
"ConnectionStrings": {
"DefaultConnection": "Server=.\\SQLEXPRESS;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true;TrustServerCertificate=true"
},
"AppSettings": {
"CompanyName": "Powder Coating Logix",
"DefaultQuoteValidityDays": 30,
"DefaultPaymentTerms": "Net 30",
"TaxRate": 0.0,
"Currency": "USD",
"TrialPeriodDays": 7,
"QuoteApprovalTokenDays": 30
},
"AI": {
"Anthropic": {
"ApiKey": "your-anthropic-api-key-here"
}
},
"SendGrid": { ... },
"Stripe": { ... },
"Storage": { ... }
}
```
**AI uses Anthropic Claude Sonnet 4.6** (`claude-sonnet-4-6`) — NOT OpenAI. The `AI:Anthropic:ApiKey` config key is what the AI photo quoting and AI scheduling services read.
### API (src/PowderCoating.Api/appsettings.json)
```json
{
"JwtSettings": {
"SecretKey": "CHANGE-THIS-TO-YOUR-OWN-SECRET-KEY-AT-LEAST-32-CHARACTERS",
"Issuer": "PowderCoatingAPI",
"Audience": "PowderCoatingMobileApp",
"ExpirationMinutes": 1440
}
}
```
### Launch Settings (src/PowderCoating.Web/Properties/launchSettings.json)
Default ports:
- HTTPS: 58461
- HTTP: 58462
## Authentication & Authorization
### System Roles
- **SuperAdmin**: Platform-wide access, sees all companies and deleted records
- **Administrator**: Company admin
- **Manager**: Operations management
- **Employee**: Create/edit jobs and quotes
- **ShopFloor**: Update job status
- **ReadOnly**: View-only access
### Custom Authorization Policies
Defined in `PowderCoating.Shared/Constants/AppConstants.cs`:
- `RequireAdministratorRole`
- `CanManageJobs`
- `CanManageInventory`
- `CanManageUsers`
- `CanViewData`
Apply with `[Authorize(Policy = "PolicyName")]` on controllers/actions.
### JWT Authentication (API Only)
API uses JWT Bearer tokens. Web uses cookie-based Identity authentication.
## AutoMapper Configuration
AutoMapper is registered as singleton in `Program.cs`:
```csharp
builder.Services.AddSingleton(provider => new MapperConfiguration(cfg =>
{
cfg.AddMaps(typeof(ApplicationAssemblyMarker).Assembly);
}).CreateMapper());
```
All profiles in `Application/Mappings/` are auto-discovered. Profiles include reverse mappings for entity ↔ DTO conversion.
## Logging
Serilog configured to write:
- Console (structured logs)
- File: `logs/powdercoating-{Date}.txt` (rolling daily)
Access via constructor injection:
```csharp
private readonly ILogger<ExampleController> _logger;
```
## Common Development Tasks
### Adding a New Entity
1. Create entity class in `Core/Entities/` inheriting from `BaseEntity`
2. Add DbSet to `ApplicationDbContext`
3. Register repository property in `IUnitOfWork` interface
4. Add lazy-loaded property in `UnitOfWork` implementation
5. Create migration: `dotnet ef migrations add AddEntityName --project ../PowderCoating.Infrastructure`
6. Apply migration: `dotnet ef database update --project ../PowderCoating.Infrastructure`
### Adding a New Controller
1. Create DTOs in `Application/DTOs/`
2. Create AutoMapper profile in `Application/Mappings/`
3. Create controller in `Web/Controllers/`
4. Create views in `Web/Views/[ControllerName]/`
5. Add navigation link in `Views/Shared/_Layout.cshtml`
### Working with Soft Deletes
```csharp
// Soft delete (sets IsDeleted = true)
await _unitOfWork.Customers.SoftDeleteAsync(id);
await _unitOfWork.CompleteAsync();
// Physical delete (use sparingly)
await _unitOfWork.Customers.DeleteAsync(entity);
await _unitOfWork.CompleteAsync();
// Include deleted records in query
var allCustomers = await _unitOfWork.Customers.GetAllAsync(ignoreQueryFilters: true);
```
### Bypassing Multi-Tenancy Filters
Only for SuperAdmin users:
```csharp
// See all companies' data
var allJobs = await _unitOfWork.Jobs.GetAllAsync(ignoreQueryFilters: true);
```
## Implemented Modules
All modules below are fully implemented with controllers, views, and migrations applied.
### Operations
- **Jobs** — full lifecycle (16 statuses), worker assignment, time entries, rework tracking, shop access codes, job templates
- **Quotes** — multi-item pricing engine, AI Photo Quoting (Anthropic Claude Sonnet 4.6), quote-to-job conversion, customer approval portal, online payment
- **Invoices** — create from job, partial payments, voids, PDF download, email send; 1:1 Job→Invoice enforced by unique index
- **Deposits** — record against customer/job/quote; auto-applied to invoices on creation; receipt PDF via QuestPDF
- **Customers** — commercial and non-commercial types, pricing tiers, tax exempt flag + certificate upload, credit limits
- **Oven Scheduler** — batch jobs into named ovens, capacity planning, suggested batches
### Inventory & Purchasing
- **Inventory** — stock tracking, transactions, reorder alerts, powder coverage/efficiency fields
- **Vendors** — supplier management, payment terms, linked to inventory items
- **Purchase Orders** — create/submit/receive POs, convert to vendor bills
- **Accounts Payable** — vendor bills, AP ledger, payment tracking
### Shop Management
- **Shop Workers** — roles (Coater, Sandblaster, etc.), assignment to jobs and maintenance tasks
- **Equipment & Maintenance** — equipment status lifecycle, scheduled/completed maintenance records
- **Catalog Items** — pre-priced service catalog with default prices
- **Pricing Tiers** — customer discount tiers; use `CompanyAdminOnly` policy (not `RequireAdministratorRole`)
### Billing & Payments
- **Stripe** — subscription plans, checkout sessions, customer portal, webhooks (`/stripe/webhook`)
- **Stripe Connect** — embedded payments, OAuth flow for tenant onboarding
- **Twilio SMS** — `ISmsService` fully implemented; webhook at `POST /Webhooks/TwilioSms`
### Platform (SuperAdmin only)
- **Platform Users** — create/manage SuperAdmin accounts
- **Companies** — view/manage all tenant companies
- **Seed Data** — manual seeding via Platform Management UI (not automatic)
- **Subscription Plans** — `SubscriptionPlanConfig` controls per-plan limits and pricing
### Other
- **Help Center** — 14 fully-written articles at `Views/Help/`
- **Setup Wizard** — 10-step onboarding wizard at `SetupWizardController`
- **Reports** — 24 report actions including P&L, AR Aging, Powder Usage, Job Cycle Time, PDF exports
- **Gift Certificates** — issue, redeem, track balance
- **Announcements** — platform-wide announcements to tenants
### Key Pricing Rules
- Custom powder (no inventory item + `PowderToOrder` > 0): charge for the **full ordered quantity**, not just calculated usage
- In-stock inventory powder: charge for calculated usage only (surface area × lbs/sqft × unit cost)
- Tax exempt customers (`Customer.IsTaxExempt`): `TaxPercent` defaults to 0 on quote and invoice create; customer dropdown marks exempt customers with ★
### Key Rules
- Custom powder (no inventory item + `PowderToOrder > 0`): charge for the **full ordered quantity**
- In-stock powder: charge for calculated usage only (surface area × lbs/sqft × unit cost)
- Tax-exempt customers (`Customer.IsTaxExempt`): `TaxPercent` defaults to 0 on quote/invoice create; marked ★ in dropdowns
### Pricing Routing Flags — Must Stay In Sync Across All Three Layers
`PricingCalculationService.CalculateQuoteItemPriceAsync` routes each item to the correct pricing path using boolean flags. **These flags MUST exist identically on `QuoteItem`, `JobItem`, and `CreateQuoteItemDto`, AND be mapped in all three `JobItemAssemblyService.CreateJobItem` overloads.**
`PricingCalculationService.CalculateQuoteItemPriceAsync` routes via boolean flags. **Must exist identically on `QuoteItem`, `JobItem`, and `CreateQuoteItemDto`, mapped in all three `JobItemAssemblyService.CreateJobItem` overloads.**
| Flag | Effect if missing on JobItem |
|------|------------------------------|
@@ -489,83 +138,77 @@ All modules below are fully implemented with controllers, views, and migrations
| `IsLaborItem` | Item repriced at surface-area rate instead of hours × labor rate |
| `IsSalesItem` | ManualUnitPrice ignored; item repriced using coat/surface math |
**Checklist when adding a new pricing routing flag:**
1. Add the property to `QuoteItem` (Core/Entities)
2. Add the property to `JobItem` (Core/Entities)
3. Add it to `CreateQuoteItemDto` (Application/DTOs)
4. Add it to `JobItemSeed` (private class in JobItemAssemblyService)
5. Map it in all three `JobItemAssemblyService.CreateJobItem` overloads
6. Include it in every `existingItemsData` JSON block in job views (`Edit.cshtml`, `EditItems.cshtml`) and in all job controller actions that build `CreateQuoteItemDto` from a `JobItem`
7. Add a migration if the field is new on a persisted entity
8. The structural test `PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem` in `JobItemAssemblyServiceTests` will fail until steps 13 are done — this is intentional
**Checklist when adding a new flag:**
1. Add to `QuoteItem` (Core/Entities)
2. Add to `JobItem` (Core/Entities)
3. Add to `CreateQuoteItemDto` (Application/DTOs)
4. Add to `JobItemSeed` (private class in `JobItemAssemblyService`)
5. Map in all three `JobItemAssemblyService.CreateJobItem` overloads
6. Include in every `existingItemsData` JSON block in `Edit.cshtml`, `EditItems.cshtml`, and all controller actions that build `CreateQuoteItemDto` from a `JobItem`
7. Add migration if field is new on a persisted entity
8. Structural test `PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem` fails until steps 13 are done — intentional
### Branding
- Application name: **Powder Coating Logix**
- PCL logo: `wwwroot/images/pcl-logo.png` — used in sidebar header (when no tenant logo), login/register pages, sidebar footer
- Sidebar footer always shows PCL logo linking to `http://www.powdercoatinglogix.com`
- Tenant companies can upload their own logo (stored in Azure Blob `companylogos` container); it replaces the PCL logo in the sidebar header
---
## Known Issues
## Configuration
- Entity Framework warnings about global query filters on related entities (non-critical, informational only)
### Key Settings (`src/PowderCoating.Web/appsettings.json`)
- DB: `ConnectionStrings:DefaultConnection` (SQL Server Express)
- AI: `AI:Anthropic:ApiKey`**Anthropic Claude `claude-sonnet-4-6`, NOT OpenAI**
- Ports: HTTPS 58461 / HTTP 58462
## File Upload Configuration
### Auth & Roles
- Web: cookie-based ASP.NET Identity. API: JWT Bearer.
- System roles: SuperAdmin, Administrator, Manager, Employee, ShopFloor, ReadOnly
- Policies in `AppConstants.cs`: `RequireAdministratorRole`, `CanManageJobs`, `CanManageInventory`, `CanManageUsers`, `CanViewData`, `CompanyAdminOnly`
- **PricingTiers use `CompanyAdminOnly` — NOT `RequireAdministratorRole`** (that policy is unregistered and will throw)
Limits defined in `AppConstants.cs`:
- Max file size: 10 MB
- Allowed extensions: jpg, jpeg, png, gif, pdf, doc, docx, xls, xlsx
### File Uploads
Limits in `AppConstants.cs`: 10 MB max, allowed: jpg/jpeg/png/gif/pdf/doc/docx/xls/xlsx.
## Testing Strategy
---
- **Unit Tests**: Test business logic in isolation
- **Integration Tests**: Test full request pipeline with test database
- Use xUnit framework
- Mock `IUnitOfWork` in unit tests
## UI Rules
## Extending the System
- **HTML entities in `.cshtml`** — `&mdash;` not `—`, `&times;` not `×`, `&hellip;` not `…`. Literal Unicode gets corrupted by AI tools + Windows file encoding.
- **External JS files only** — put scripts in `wwwroot/js/*.js`, reference via `src=`. Inline `@section Scripts` blocks can silently fail with SyntaxErrors from layout HTML context.
- **`alert-permanent` CSS class** — `_Layout` auto-dismisses `.alert:not(.alert-permanent)` after ~5s. Any non-toast alert that must persist needs this class.
- **SignalR hubs already in place**: `NotificationHub``/hubs/notifications` (company-scoped), `ShopHub``/hubs/shop` (shop floor).
### Adding AI Features
---
AI uses Anthropic Claude Sonnet 4.6 via `IAiQuoteService`. Configure the key under `AI:Anthropic:ApiKey` in `appsettings.json`.
1. Create service interface in `Application/Interfaces/`
2. Implement in `Infrastructure/Services/` calling the Anthropic client
3. Inject into controllers via DI
## Gotchas
### SignalR Hubs
- **Two data export controllers**: `DataExportController` (SuperAdmin) and `AccountDataExportController` (company self-service). When changing CSV columns, fix **both**.
- **Help docs**: when a feature changes, update both `HelpKnowledgeBase.cs` (AI assistant knowledge) and the matching article in `Views/Help/` (human-readable help center).
- **Demo reset**: `DemoController.ResetDemoData` is gated on `company.CompanyCode == "DEMO"` — only the demo tenant can trigger a reset. ForceRemoveAll wipes all company data before reseeding.
- **artemis@ account**: the "break glass" root SuperAdmin — guards in `PlatformUsersController` protecting it are intentional, never remove them.
Two hubs are already implemented and mapped in `Program.cs`:
- `NotificationHub``/hubs/notifications` (company-scoped push notifications)
- `ShopHub``/hubs/shop` (real-time shop floor updates)
---
To add a new hub:
1. Create hub class in `Web/Hubs/`
2. Map hub in `Program.cs`: `app.MapHub<YourHub>("/hubpath")`
3. Use JavaScript client in views to connect
## Implemented Modules
### Adding API Endpoints
All fully implemented with controllers, views, and migrations applied.
1. Create controller in `Api/Controllers/` with `[ApiController]` attribute
2. Return `ActionResult<T>` types
3. Use `[Authorize]` for protected endpoints
4. Document with XML comments for Swagger
**Operations**: Jobs (16 statuses, worker assignment, time entries, rework, shop codes, templates) · Quotes (AI Photo Quoting via Anthropic, quote→job conversion, customer approval portal) · Invoices (1:1 Job→Invoice unique index; partial payments, void, PDF, email) · Deposits (auto-applied on invoice create; QuestPDF receipt) · Customers (commercial/non-commercial, pricing tiers, tax-exempt + cert upload) · Oven Scheduler (named ovens, capacity, suggested batches)
## Project Dependencies
**Inventory & Purchasing**: Inventory (transactions, reorder alerts, powder coverage/efficiency) · Vendors · Purchase Orders (create/submit/receive, convert to bills) · Accounts Payable (bills, AP ledger, payment tracking)
Key NuGet packages:
- **AutoMapper 16.0.0**: Entity-to-DTO mapping
- **Entity Framework Core 8.0.11**: ORM and database access
- **Serilog.AspNetCore 8.0.3**: Structured logging
- **Microsoft.AspNetCore.Identity.UI 8.0.11**: Authentication
- **Swashbuckle.AspNetCore 7.2.0**: API documentation (API project)
**Shop Management**: Shop Workers (roles, job/maintenance assignment) · Equipment & Maintenance · Catalog Items · Pricing Tiers
## Security Considerations
**Billing**: Stripe (subscriptions, checkout sessions, webhooks `/stripe/webhook`) · Stripe Connect (embedded payments, OAuth) · Twilio SMS (`ISmsService`; webhook `POST /Webhooks/TwilioSms`)
- Password requirements: 8+ chars, uppercase, lowercase, digit
- HTTPS enforced in production
- SQL injection prevented by EF Core parameterization
- XSS protection via Razor encoding
- CSRF tokens on all forms (automatic with ASP.NET Core)
- Sensitive settings (connection strings, API keys) should use User Secrets in development and Azure Key Vault in production
**Platform (SuperAdmin)**: Platform Users · Companies · Seed Data (manual only) · Subscription Plans (`SubscriptionPlanConfig`)
## Active design work
A visual redesign is in progress. If the user asks about UI changes, dashboard/jobs/board styling, or the new design tokens, read `design_handoff_pcl_redesign/README.md` and follow `design_handoff_pcl_redesign/CLAUDE.md` for that work.
**Other**: Help Center (14 articles at `Views/Help/`) · Setup Wizard (10-step, `SetupWizardController`) · Reports (24 actions: P&L, AR Aging, Powder Usage, Cycle Time, PDF exports) · Gift Certificates · Announcements · In-App Notification Bell · Passkey/Biometric Login (WebAuthn, Fido2NetLib) · Customer Intake Kiosk (iPad, SignalR push, `KioskSession`) · AI Accounting Features (receipt scan, AR follow-up, smart categorization, cash flow forecast, anomaly detection)
---
## Branding
- App name: **Powder Coating Logix**
- PCL logo: `wwwroot/images/pcl-logo.png` — sidebar header (when no tenant logo), login/register, sidebar footer (always)
- Sidebar footer always links to `http://www.powdercoatinglogix.com`
- Tenant logos: Azure Blob `companylogos` container; replaces PCL logo in sidebar header only
## Active Design Work
A visual redesign is in progress. For UI changes, dashboard/jobs/board styling, or design tokens: read `design_handoff_pcl_redesign/README.md` and follow `design_handoff_pcl_redesign/CLAUDE.md`.
-286
View File
@@ -1,286 +0,0 @@
# CSV Bulk Import Feature - Implementation Summary
## Overview
Comprehensive CSV bulk import feature for Powder Coating App with template generation, validation, and error reporting.
## Components Implemented
### 1. DTOs (Application/DTOs/Import/)
#### `CsvImportResultDto.cs`
- Properties: Success, SuccessCount, ErrorCount, TotalRows, Errors, Warnings
- Summary property for user-friendly display
#### `CustomerImportDto.cs`
- Fields: CompanyName, ContactName, Email, Phone, Address, City, State, ZipCode
- Business: CustomerType, PricingTierCode, CreditLimit, PaymentTerms, TaxExempt
- Additional: Notes
- Uses CsvHelper attributes for CSV mapping
#### `CatalogItemImportDto.cs`
- Fields: CategoryPath (hierarchical, e.g., "Automotive/Wheels"), ItemName, SKU
- Details: Description, BasePrice, UnitOfMeasure
- Specifications: EstimatedWeight, EstimatedSurfaceArea
- Flags: RequiresSandblasting, RequiresMasking, IsActive
- Auto-creates categories on import if they don't exist
#### `InventoryItemImportDto.cs`
- Fields: SKU, ItemName, CategoryName, Manufacturer
- Color: ColorName, ColorCode
- Inventory: QuantityInStock, UnitOfMeasure, UnitCost
- Reordering: ReorderPoint, ReorderQuantity
- Additional: Notes
### 2. Service Interface (Application/Interfaces/ICsvImportService.cs)
Methods:
- `byte[] GenerateCustomerTemplate()` - Creates CSV template with example data
- `byte[] GenerateCatalogItemTemplate()` - Creates catalog template with 2 examples
- `byte[] GenerateInventoryItemTemplate()` - Creates inventory template with 2 examples
- `Task<CsvImportResultDto> ImportCustomersAsync(Stream, companyId)` - Import customers
- `Task<CsvImportResultDto> ImportCatalogItemsAsync(Stream, companyId)` - Import catalog items
- `Task<CsvImportResultDto> ImportInventoryItemsAsync(Stream, companyId)` - Import inventory items
### 3. Service Implementation (Infrastructure/Services/CsvImportService.cs)
#### Template Generation
- Uses CsvHelper library for CSV writing
- Includes headers and example rows
- Returns byte array for direct download
#### Import Logic
- **Validation**: Required fields, file format, data types
- **Duplicate Detection**:
- Customers: By email (case-insensitive)
- Catalog Items: By SKU (case-insensitive)
- Inventory Items: By SKU (case-insensitive)
- **Pricing Tier Resolution**: Looks up by TierName (Standard, Silver, Gold, Platinum)
- **Category Auto-Creation**: Parses CategoryPath (e.g., "Automotive/Wheels") and creates parent/child hierarchy
- **Error Handling**: Row-by-row with detailed error messages
- **Transaction Support**: Uses UnitOfWork for atomic commits
#### Key Features
- Multi-tenancy: All imports filtered by CompanyId
- Soft Delete Support: Uses global query filters
- Comprehensive Logging: Success/error counts, detailed messages
- Warnings vs Errors: Non-fatal issues reported as warnings
### 4. Controller Updates (Web/Controllers/ToolsController.cs)
Added 6 new actions:
#### Template Downloads (GET)
- `DownloadCustomerTemplate()` - Returns customer_import_template_{timestamp}.csv
- `DownloadCatalogTemplate()` - Returns catalog_import_template_{timestamp}.csv
- `DownloadInventoryTemplate()` - Returns inventory_import_template_{timestamp}.csv
#### CSV Imports (POST, ValidateAntiForgeryToken)
- `CsvImportCustomers(IFormFile)` - Imports customers from CSV
- `CsvImportCatalogItems(IFormFile)` - Imports catalog items from CSV
- `CsvImportInventoryItems(IFormFile)` - Imports inventory items from CSV
All import actions:
- Validate file extension (.csv only)
- Check company association (multi-tenancy)
- Return JSON with detailed results
- Log operations
### 5. View Updates (Web/Views/Tools/Index.cshtml)
Added "CSV Bulk Import" card with:
#### Tabbed Interface
- 3 tabs: Customers, Catalog Items, Inventory
- Each tab contains:
- **Download Section**: Template download button
- **Upload Section**: File input + Import button
- **Results Section**: Dynamic display of import results
#### UI Features
- Bootstrap 5 styling with color-coded tabs (primary, success, info)
- File validation (CSV only)
- Loading spinners during import
- Toast notifications for success/error feedback
- Detailed error/warning lists
### 6. JavaScript (Web/wwwroot/js/bulk-import.js)
Features:
- **AJAX Upload**: Non-blocking file uploads with fetch API
- **Validation**: File type (.csv), file size (10MB max)
- **Progress Indicators**: Spinners, disabled buttons during upload
- **Results Display**:
- Card with success/danger styling
- Stats: Imported, Errors, Total Rows
- Detailed error/warning lists
- **Toast Notifications**: Success/error messages
- **Security**: HTML escaping, anti-forgery tokens
### 7. Service Registration (Program.cs)
Added:
```csharp
builder.Services.AddScoped<ICsvImportService, CsvImportService>();
```
## Dependencies
### NuGet Packages Added
- **CsvHelper 33.1.0** (Infrastructure project)
- **CsvHelper 33.1.0** (Application project)
Already available:
- AutoMapper, Entity Framework Core, Serilog
## Architecture Patterns
### Clean Architecture
- **Domain Layer** (Core): Entities remain unchanged
- **Application Layer**: DTOs, Service Interfaces
- **Infrastructure Layer**: Service Implementations
- **Presentation Layer**: Controllers, Views, JavaScript
### Design Patterns Used
- **Repository Pattern**: Via IUnitOfWork
- **Unit of Work**: Transaction management
- **Dependency Injection**: All services registered
- **DTO Pattern**: Separation of concerns
- **Template Method**: Shared import logic structure
## Edge Cases Handled
### File Validation
- Empty files → Error message
- Invalid format (not CSV) → Rejected with message
- File too large (>10MB) → Client-side validation error
- Missing headers → CsvHelper configuration handles gracefully
### Data Validation
- Missing required fields → Row skipped with error
- Duplicate records → Warning, row skipped
- Invalid data types → Exception caught, row skipped
- Invalid foreign keys (pricing tier) → Warning, continues with null
### Category Auto-Creation
- Hierarchical paths (e.g., "Automotive/Wheels/16-inch")
- Missing parent categories → Auto-created recursively
- Existing categories → Reused (no duplicates)
- Cache to avoid redundant DB queries
### Multi-Tenancy
- All queries filtered by CompanyId
- Global query filters automatically applied
- Users can only import to their own company
## Usage Examples
### Customer Import Template
```csv
CompanyName,ContactName,Email,Phone,Address,City,State,ZipCode,CustomerType,PricingTierCode,CreditLimit,PaymentTerms,TaxExempt,Notes
Example Company Inc.,John Doe,john@example.com,555-1234,123 Main St,Springfield,IL,62701,Commercial,Gold,5000,Net 30,false,Sample customer
```
### Catalog Item Import Template
```csv
CategoryPath,ItemName,SKU,Description,BasePrice,UnitOfMeasure,EstimatedWeight,EstimatedSurfaceArea,RequiresSandblasting,RequiresMasking,IsActive
Automotive/Wheels,Car Wheel - Standard 16",WHL-16-STD,Standard 16 inch car wheel,75.00,each,15.0,4.5,true,true,true
Industrial/Railings,Handrail - 10 ft section,RAIL-10FT,10 foot handrail section,150.00,section,25.0,12.0,true,false,true
```
### Inventory Item Import Template
```csv
SKU,ItemName,CategoryName,Manufacturer,ColorName,ColorCode,QuantityInStock,UnitOfMeasure,UnitCost,ReorderPoint,ReorderQuantity,Notes
PWD-BLK-001,Black Powder Coating,Powder Coatings,Tiger Drylac,Black,RAL 9005,500,lbs,3.50,100,200,Glossy finish
PWD-WHT-001,White Powder Coating,Powder Coatings,Tiger Drylac,White,RAL 9010,350,lbs,3.75,75,150,Bright white
```
## Testing Checklist
### Functional Tests
- [ ] Download all 3 templates
- [ ] Verify template format and example data
- [ ] Import valid CSV files
- [ ] Import with missing required fields
- [ ] Import with duplicate records
- [ ] Import with invalid pricing tier codes
- [ ] Import catalog items with nested categories
- [ ] Verify multi-tenancy (users see only their company data)
- [ ] Verify error messages are clear and actionable
- [ ] Verify success counts are accurate
### UI/UX Tests
- [ ] Tabs switch correctly
- [ ] File validation works (reject non-CSV)
- [ ] Loading spinners display during import
- [ ] Results display correctly (success/error cards)
- [ ] Toast notifications appear
- [ ] Error lists are readable
- [ ] Warning lists display separately
### Security Tests
- [ ] Anti-forgery tokens validated
- [ ] Company isolation enforced
- [ ] File size limits enforced
- [ ] SQL injection prevented (parameterized queries via EF)
- [ ] XSS prevented (HTML escaping in JS)
### Performance Tests
- [ ] Large files (1000+ rows)
- [ ] Duplicate detection with large datasets
- [ ] Category creation with deep nesting
- [ ] Concurrent imports
## Known Limitations
1. **No Update Logic**: Existing records are skipped, not updated
2. **No Transaction Rollback UI**: Errors are reported but successful rows are committed
3. **No Progress Bar**: Large files show spinner but no percentage
4. **No Preview**: Users can't preview data before importing
5. **No Batch Processing**: All rows processed in single transaction
## Future Enhancements
1. **Update Mode**: Allow updating existing records by email/SKU
2. **Dry Run**: Preview import results without committing
3. **Progress Bar**: Real-time progress for large imports
4. **Batch Processing**: Split large imports into chunks
5. **Export Current Data**: Download existing data as CSV
6. **Column Mapping**: Allow users to map custom CSV columns
7. **Validation Report**: Pre-import validation before committing
8. **Undo Import**: Rollback capability for recent imports
9. **Import History**: Track all imports with timestamps
10. **Scheduled Imports**: Automate recurring imports
## Files Created/Modified
### Created
- `src/PowderCoating.Application/DTOs/Import/CsvImportResultDto.cs`
- `src/PowderCoating.Application/DTOs/Import/CustomerImportDto.cs`
- `src/PowderCoating.Application/DTOs/Import/CatalogItemImportDto.cs`
- `src/PowderCoating.Application/DTOs/Import/InventoryItemImportDto.cs`
- `src/PowderCoating.Application/Interfaces/ICsvImportService.cs`
- `src/PowderCoating.Infrastructure/Services/CsvImportService.cs`
- `src/PowderCoating.Web/wwwroot/js/bulk-import.js`
### Modified
- `src/PowderCoating.Web/Controllers/ToolsController.cs` (added 6 actions + DI)
- `src/PowderCoating.Web/Views/Tools/Index.cshtml` (added CSV import UI)
- `src/PowderCoating.Web/Program.cs` (registered ICsvImportService)
- `src/PowderCoating.Application/PowderCoating.Application.csproj` (added CsvHelper)
- `src/PowderCoating.Infrastructure/PowderCoating.Infrastructure.csproj` (added CsvHelper)
## Build Status
**Build Succeeded** - 0 Errors, 0 Warnings (related to CSV import feature)
## Conclusion
The CSV bulk import feature is fully implemented and ready for testing. It provides:
- Easy template downloads for users
- Robust validation and error handling
- Multi-tenancy support
- Category auto-creation for catalog items
- Comprehensive error reporting
- Clean, user-friendly interface
The implementation follows Clean Architecture principles, uses existing infrastructure (UnitOfWork, Repository pattern), and integrates seamlessly with the existing Powder Coating App.
-109
View File
@@ -1,109 +0,0 @@
# Customer Entity Fix - Duplicate Notes Field Resolved
## Issue Found
The `Customer` entity had a duplicate `Notes` field:
1. **Collection property**: `ICollection<CustomerNote> Notes` - for related CustomerNote entities
2. **String property**: `string? Notes` - for general notes text
This would have caused a compilation error and database schema issues.
## Fix Applied
### Changed in Customer Entity (`src/PowderCoating.Core/Entities/Customer.cs`)
**Before:**
```csharp
public virtual ICollection<CustomerNote> Notes { get; set; } = new List<CustomerNote>();
public string? Notes { get; set; }
```
**After:**
```csharp
public virtual ICollection<CustomerNote> CustomerNotes { get; set; } = new List<CustomerNote>();
public string? GeneralNotes { get; set; }
```
### Changes Summary:
1. **Collection renamed**: `Notes``CustomerNotes` (more descriptive)
2. **String property renamed**: `Notes``GeneralNotes` (avoids conflict)
### Updated DTOs
All Customer DTOs have been updated to use `GeneralNotes`:
- `CustomerDto.GeneralNotes`
- `CreateCustomerDto.GeneralNotes`
- `UpdateCustomerDto.GeneralNotes` (inherited)
### Database Impact
When you create your first migration, the database will have:
- **Customers.GeneralNotes** column (string) - for quick notes about the customer
- **CustomerNotes** table (separate) - for detailed, timestamped notes with relationships
### Usage Pattern
#### General Notes (Simple text field):
```csharp
var customer = new Customer
{
CompanyName = "ABC Corp",
GeneralNotes = "Preferred customer, always pays on time"
};
```
#### Customer Notes (Detailed note entries):
```csharp
var note = new CustomerNote
{
CustomerId = customer.Id,
Note = "Called to discuss new project requirements",
IsImportant = true
};
customer.CustomerNotes.Add(note);
```
### When to Use Each:
**GeneralNotes (string):**
- Quick reference information
- General reminders
- Brief customer preferences
- Single-line notes
**CustomerNotes (collection):**
- Detailed interaction history
- Time-stamped communication logs
- Multiple notes over time
- Important flags and tracking
## No Action Required
This fix is already applied in the updated project files. When you:
1. Run `dotnet ef migrations add InitialCreate`
2. The migration will create the correct schema with `GeneralNotes` column
## Benefits of This Fix
**No naming conflicts** - Clear distinction between the two properties
**Better semantics** - `CustomerNotes` clearly indicates a collection
**Clearer intent** - `GeneralNotes` indicates simple text vs. complex notes
**Follows conventions** - Collection names are typically plural nouns
**Database ready** - Will generate proper schema without conflicts
## Files Modified
1. `/src/PowderCoating.Core/Entities/Customer.cs`
2. `/src/PowderCoating.Application/DTOs/Customer/CustomerDtos.cs`
## Next Steps
When you first run the application and create migrations, you'll see:
```bash
dotnet ef migrations add InitialCreate --project src/PowderCoating.Infrastructure --startup-project src/PowderCoating.Web
```
The migration will correctly create:
- `Customers` table with `GeneralNotes` column
- `CustomerNotes` table with foreign key to `Customers`
Everything is now consistent and ready to build!
-66
View File
@@ -1,66 +0,0 @@
-- =============================================
-- Delete All Customers for Testing
-- WARNING: This will delete ALL customer data!
-- =============================================
USE PowderCoatingDb;
GO
BEGIN TRANSACTION;
BEGIN TRY
PRINT 'Starting customer deletion...';
-- Count customers before deletion
DECLARE @CustomerCount INT;
SELECT @CustomerCount = COUNT(*) FROM Customers;
PRINT 'Found ' + CAST(@CustomerCount AS VARCHAR) + ' customers to delete';
-- Option 1: Delete related data first (safest)
-- Update Jobs to remove customer references
PRINT 'Removing customer references from Jobs...';
UPDATE Jobs SET CustomerId = NULL WHERE CustomerId IS NOT NULL;
-- Update Quotes to remove customer references
PRINT 'Removing customer references from Quotes...';
UPDATE Quotes SET CustomerId = NULL WHERE CustomerId IS NOT NULL;
-- Delete all customers (hard delete)
PRINT 'Deleting all customers...';
DELETE FROM Customers;
-- Verify deletion
SELECT @CustomerCount = COUNT(*) FROM Customers;
PRINT 'Remaining customers: ' + CAST(@CustomerCount AS VARCHAR);
PRINT 'Customer deletion completed successfully!';
COMMIT TRANSACTION;
PRINT 'Transaction committed.';
END TRY
BEGIN CATCH
PRINT 'Error occurred: ' + ERROR_MESSAGE();
ROLLBACK TRANSACTION;
PRINT 'Transaction rolled back.';
END CATCH;
GO
-- Verify the results
SELECT
'Customers' AS TableName,
COUNT(*) AS RecordCount
FROM Customers
UNION ALL
SELECT
'Jobs with NULL CustomerId',
COUNT(*)
FROM Jobs
WHERE CustomerId IS NULL
UNION ALL
SELECT
'Quotes with NULL CustomerId',
COUNT(*)
FROM Quotes
WHERE CustomerId IS NULL;
GO
-3119
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
<Project>
<PropertyGroup>
<!--
NCalc2 2.1.0 -> Antlr4 4.6.4 -> Antlr4.Runtime -> NETStandard.Library 1.6.0 pulls in
old package versions that trigger NU1605 downgrade warnings when publishing for linux-x64.
These are harmless false positives — .NET 8 supplies all of these natively at runtime.
Suppressing NU1605 here is cleaner than pinning every affected transitive package individually.
-->
<NoWarn>$(NoWarn);NU1605</NoWarn>
</PropertyGroup>
<!--
TRACKED: System.Security.Cryptography.Xml 8.0.2 has two High advisories (GHSA-37gx-xxp4-5rgx,
GHSA-w3x6-4m5h-cxqf — XML signature vulnerabilities). No patched version exists in the NuGet
feed as of 2026-06-14; 9.0.0 (the only higher version) is also flagged. Re-check when a
patched 8.x or 9.x build ships and pin here. Pulled in transitively by one of: Fido2, EPPlus,
Azure SDK, or VisualStudio.Web.CodeGeneration.Design.
-->
</Project>
-369
View File
@@ -1,369 +0,0 @@
# Final Update Summary - AutoMapper 16.0 Without Extensions
## ✅ All Changes Completed
### 1. **Removed AutoMapper.Extensions.Microsoft.DependencyInjection**
- Replaced with direct **AutoMapper 16.0** package
- Manual configuration using `MapperConfiguration`
- Singleton registration for better performance
### 2. **All Projects Reverted to .NET 8.0 LTS**
- ✅ PowderCoating.Core → net8.0
- ✅ PowderCoating.Application → net8.0
- ✅ PowderCoating.Infrastructure → net8.0
- ✅ PowderCoating.Web → net8.0
- ✅ PowderCoating.Api → net8.0
- ✅ PowderCoating.Shared → net8.0
- ✅ PowderCoating.UnitTests → net8.0
- ✅ PowderCoating.IntegrationTests → net8.0
### 3. **AutoMapper Configuration**
#### Web Project (`PowderCoating.Web/Program.cs`)
```csharp
using AutoMapper;
using PowderCoating.Application.Mappings;
// Configure AutoMapper
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile<CustomerProfile>();
mc.AddProfile<JobProfile>();
});
builder.Services.AddSingleton(mapperConfig.CreateMapper());
```
#### API Project (`PowderCoating.Api/Program.cs`)
```csharp
using AutoMapper;
using PowderCoating.Application.Mappings;
// Configure AutoMapper
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile<CustomerProfile>();
mc.AddProfile<JobProfile>();
});
builder.Services.AddSingleton(mapperConfig.CreateMapper());
```
### 4. **AutoMapper Profiles Created**
#### CustomerProfile.cs
**Location:** `src/PowderCoating.Application/Mappings/CustomerProfile.cs`
Maps:
- Customer ↔ CustomerDto
- CreateCustomerDto → Customer
- UpdateCustomerDto → Customer
- Customer → CustomerListDto (with contact name formatting)
#### JobProfile.cs
**Location:** `src/PowderCoating.Application/Mappings/JobProfile.cs`
Maps:
- Job ↔ JobDto
- CreateJobDto → Job
- UpdateJobDto → Job
- Job → JobListDto
- JobItem ↔ JobItemDto
- CreateJobItemDto → JobItem
- Job → ShopFloorJobDto (with priority colors & next steps)
**Smart Features:**
- Priority color coding (Rush=danger, Urgent=warning, etc.)
- Next step suggestions based on job status
- Enum name formatting ("InPreparation" → "In Preparation")
### 5. **Package Updates**
All packages updated to stable .NET 8.0 versions:
| Package | Version |
|---------|---------|
| AutoMapper | 16.0.0 |
| Microsoft.AspNetCore.Identity.UI | 8.0.0 |
| Microsoft.EntityFrameworkCore | 8.0.0 |
| Microsoft.EntityFrameworkCore.SqlServer | 8.0.0 |
| Microsoft.EntityFrameworkCore.Design | 8.0.0 |
| Microsoft.AspNetCore.Authentication.JwtBearer | 8.0.0 |
| Swashbuckle.AspNetCore | 6.5.0 |
| Serilog.AspNetCore | 8.0.0 |
| FluentValidation | 11.9.0 |
| Microsoft.SemanticKernel | 1.0.1 |
| Microsoft.ML | 3.0.1 |
## 📦 What's Changed from Previous Version
### Before:
```csharp
// Used extension package
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
```
### After:
```csharp
// Manual configuration with explicit profile registration
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile<CustomerProfile>();
mc.AddProfile<JobProfile>();
});
builder.Services.AddSingleton(mapperConfig.CreateMapper());
```
## 🎯 Benefits of This Approach
### 1. **No Extension Package Dependency**
- Direct AutoMapper 16.0 reference only
- Simpler dependency tree
- Better control over configuration
### 2. **Explicit Profile Registration**
- Know exactly which profiles are registered
- Easier to debug
- Better IntelliSense support
### 3. **Singleton Registration**
- Better performance (mapper created once)
- Thread-safe
- Recommended by AutoMapper team
### 4. **Compile-Time Safety**
- Errors caught at compile time
- No runtime profile discovery issues
- Clear configuration errors
## 🚀 How to Add New Profiles
When you add new features, follow this pattern:
### Step 1: Create Profile Class
Create in `src/PowderCoating.Application/Mappings/`:
```csharp
using AutoMapper;
using PowderCoating.Core.Entities;
using PowderCoating.Application.DTOs.YourModule;
namespace PowderCoating.Application.Mappings;
public class YourModuleProfile : Profile
{
public YourModuleProfile()
{
CreateMap<YourEntity, YourDto>();
CreateMap<CreateYourDto, YourEntity>();
// Add more mappings...
}
}
```
### Step 2: Register in Both Program.cs Files
**In Web/Program.cs:**
```csharp
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile<CustomerProfile>();
mc.AddProfile<JobProfile>();
mc.AddProfile<YourModuleProfile>(); // ← Add this line
});
```
**In Api/Program.cs:**
```csharp
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile<CustomerProfile>();
mc.AddProfile<JobProfile>();
mc.AddProfile<YourModuleProfile>(); // ← Add this line
});
```
## 🔍 Verification Steps
### 1. Extract the Archive
```bash
# Windows
Expand-Archive PowderCoatingApp.zip -DestinationPath C:\Projects\
# Mac/Linux
tar -xzf PowderCoatingApp.tar.gz -C ~/Projects/
```
### 2. Restore Packages
```bash
cd PowderCoatingApp
dotnet restore
```
**Expected Output:**
```
Restore succeeded.
```
### 3. Build the Solution
```bash
dotnet build
```
**Expected Output:**
```
Build succeeded.
0 Warning(s)
0 Error(s)
```
### 4. Verify AutoMapper Configuration
When you run the application, AutoMapper will validate all mappings at startup. Any configuration errors will be caught immediately.
## 📝 Usage Examples
### In Controllers
```csharp
public class CustomersController : Controller
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public CustomersController(IUnitOfWork unitOfWork, IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<IActionResult> Index()
{
var customers = await _unitOfWork.Customers.GetAllAsync();
var customerDtos = _mapper.Map<List<CustomerListDto>>(customers);
return View(customerDtos);
}
}
```
### In API Controllers
```csharp
[ApiController]
[Route("api/[controller]")]
public class JobsController : ControllerBase
{
private readonly IMapper _mapper;
public JobsController(IMapper mapper)
{
_mapper = mapper;
}
[HttpGet]
public async Task<ActionResult<List<JobListDto>>> GetAll()
{
var jobs = await _unitOfWork.Jobs.GetAllAsync();
return Ok(_mapper.Map<List<JobListDto>>(jobs));
}
}
```
## ⚠️ Important Notes
### AutoMapper Validation
AutoMapper validates configurations at startup. If you see an error like:
```
AutoMapper.AutoMapperConfigurationException: Unmapped members were found.
```
This means a mapping is incomplete. Check:
1. All DTOs have corresponding mappings
2. Property names match or are explicitly mapped
3. Complex mappings have custom resolvers
### Adding Collections
When mapping collections, AutoMapper handles it automatically:
```csharp
var customers = await _unitOfWork.Customers.GetAllAsync();
var dtos = _mapper.Map<List<CustomerDto>>(customers); // Works automatically
```
### Nested Mappings
AutoMapper automatically maps nested objects if they have profiles:
```csharp
// If Job has Customer property and both have profiles, this works:
var jobDto = _mapper.Map<JobDto>(job); // Automatically maps job.Customer
```
## 🐛 Troubleshooting
### Error: "Type 'CustomerProfile' not found"
**Solution:** Add using statement:
```csharp
using PowderCoating.Application.Mappings;
```
### Error: "No parameterless constructor defined"
**Solution:** Ensure profile classes have no constructor parameters:
```csharp
public class CustomerProfile : Profile
{
public CustomerProfile() // ← Must be parameterless
{
// Configuration...
}
}
```
### Build Error: Package version conflicts
**Solution:** Clean and restore:
```bash
dotnet clean
dotnet nuget locals all --clear
dotnet restore
dotnet build
```
## 📋 File Changes Summary
### Modified Files:
1. **src/PowderCoating.Web/PowderCoating.Web.csproj** - Package updates
2. **src/PowderCoating.Web/Program.cs** - Manual AutoMapper config
3. **src/PowderCoating.Api/PowderCoating.Api.csproj** - Package updates
4. **src/PowderCoating.Api/Program.cs** - Manual AutoMapper config
5. **src/PowderCoating.Core/PowderCoating.Core.csproj** - .NET 8.0
6. **src/PowderCoating.Application/PowderCoating.Application.csproj** - .NET 8.0, AutoMapper 16.0
7. **src/PowderCoating.Infrastructure/PowderCoating.Infrastructure.csproj** - .NET 8.0
8. **src/PowderCoating.Shared/PowderCoating.Shared.csproj** - .NET 8.0
9. **tests/PowderCoating.UnitTests/PowderCoating.UnitTests.csproj** - .NET 8.0
10. **tests/PowderCoating.IntegrationTests/PowderCoating.IntegrationTests.csproj** - .NET 8.0
### New Files:
1. **src/PowderCoating.Application/Mappings/CustomerProfile.cs**
2. **src/PowderCoating.Application/Mappings/JobProfile.cs**
## ✨ Key Improvements
**Cleaner Dependencies** - No Extensions package needed
**Explicit Configuration** - Clear what's registered
**Better Performance** - Singleton mapper instance
**Type Safety** - Compile-time profile validation
**Easier Debugging** - Clear error messages
**.NET 8.0 LTS** - Long-term support until 2026
**AutoMapper 16.0** - Latest features and performance
## 🎉 Ready to Use!
Your project now has:
- ✅ AutoMapper 16.0 without Extensions package
- ✅ Manual configuration for full control
- ✅ All projects on .NET 8.0 LTS
- ✅ Complete mapping profiles for Customer and Job modules
- ✅ Smart features (priority colors, next steps, formatting)
**Download, extract, and build - it's ready to go!**
When you add new features, just:
1. Create a new Profile class
2. Add it to MapperConfiguration in both Program.cs files
3. That's it!
---
**Note:** When .NET 10.0 releases in November 2025, you can upgrade by simply changing `<TargetFramework>net8.0</TargetFramework>` to `net10.0` in all .csproj files and updating package versions.
-132
View File
@@ -1,132 +0,0 @@
# Fix Customer Email Duplicate Error
## Problem
The unique index on `Customers.Email` was enforcing **global uniqueness** (across all companies), but in a multi-tenant system, different companies should be able to have customers with the same email address.
## Solution
Change the unique index to be scoped to `CompanyId`, allowing the same email across different companies while still preventing duplicates within the same company.
---
## Quick Fix (Run SQL Script)
### Step 1: Run the SQL Script
1. Open **SQL Server Management Studio**
2. Connect to your testing server
3. Open the file: **`fix-customer-email-index.sql`**
4. **Execute** the script
This will:
- ✅ Drop the old global unique index
- ✅ Create new company-scoped unique index
- ✅ Show verification that it worked
### Step 2: Test Seeding
1. Go to your web app: `/SeedData`
2. Click **"Seed Company Data"**
3. Should work perfectly now! ✨
---
## Alternative: Use EF Migration (For Production Deployment)
If you want to use EF migrations for a cleaner deployment:
### From Web Project Directory:
```bash
cd src/PowderCoating.Web
# Apply the migration
dotnet ef database update --project ../PowderCoating.Infrastructure
```
This will apply the migration: **`FixCustomerEmailIndexForMultiTenancy`**
---
## What Changed
### Before (Old Index):
```sql
CREATE UNIQUE INDEX IX_Customers_Email ON Customers (Email)
WHERE [Email] IS NOT NULL
```
❌ Problem: Only ONE customer across ALL companies can have `john.smith@acmemfg.com`
### After (New Index):
```sql
CREATE UNIQUE INDEX IX_Customers_Email ON Customers (CompanyId, Email)
WHERE [Email] IS NOT NULL AND [IsDeleted] = 0
```
✅ Solution: EACH company can have a customer with `john.smith@acmemfg.com`
---
## Examples
### Now This Works:
| CompanyId | Email | Status |
|-----------|-------|--------|
| 1 | john.smith@acmemfg.com | ✅ OK |
| 2 | john.smith@acmemfg.com | ✅ OK (different company) |
| 1 | jane.doe@example.com | ✅ OK |
### This Still Prevents Duplicates:
| CompanyId | Email | Status |
|-----------|-------|--------|
| 1 | john.smith@acmemfg.com | ✅ First insert OK |
| 1 | john.smith@acmemfg.com | ❌ DUPLICATE (same company) |
---
## Verification
After running the script, verify the index:
```sql
-- Check the new index definition
SELECT
i.name AS IndexName,
i.is_unique AS IsUnique,
STRING_AGG(COL_NAME(ic.object_id, ic.column_id), ', ') AS IndexColumns,
i.filter_definition AS Filter
FROM sys.indexes i
INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
WHERE i.object_id = OBJECT_ID('Customers')
AND i.name = 'IX_Customers_Email'
GROUP BY i.name, i.is_unique, i.filter_definition
```
**Expected Result:**
- IndexName: `IX_Customers_Email`
- IsUnique: `1` (true)
- IndexColumns: `CompanyId, Email`
- Filter: `[Email] IS NOT NULL AND [IsDeleted] = 0`
---
## Build & Deploy
The migration is already in your code:
```
src/PowderCoating.Infrastructure/Migrations/
└─ 20260211160000_FixCustomerEmailIndexForMultiTenancy.cs
```
When you deploy to production:
```bash
dotnet ef database update --project ../PowderCoating.Infrastructure
```
Will automatically apply this migration.
---
## Summary
**Index Fixed** - Scoped to CompanyId
**Multi-Tenancy Safe** - Same email OK across companies
**Duplicate Prevention** - Still blocks duplicates within a company
**Soft Delete Aware** - Ignores deleted records
You're ready to seed! 🎉
-215
View File
@@ -1,215 +0,0 @@
# Foreign Key Type Fix - Identity User Relationships
## 🐛 Issue Found
**Error:** Foreign key type mismatch when creating database migrations.
**Root Cause:** Entity foreign keys pointing to `ApplicationUser` were defined as `int?` but ASP.NET Identity uses `string` as the primary key type.
## ✅ Fixes Applied
### 1. Job.AssignedEmployeeId
**File:** `src/PowderCoating.Core/Entities/Job.cs`
**Before:**
```csharp
public int? AssignedEmployeeId { get; set; }
```
**After:**
```csharp
public string? AssignedEmployeeId { get; set; } // Changed for Identity FK
```
### 2. Quote.PreparedById
**File:** `src/PowderCoating.Core/Entities/Quote.cs`
**Before:**
```csharp
public int? PreparedById { get; set; }
```
**After:**
```csharp
public string? PreparedById { get; set; } // Changed for Identity FK
```
### 3. MaintenanceRecord.PerformedById
**File:** `src/PowderCoating.Core/Entities/Equipment.cs`
**Before:**
```csharp
public int? PerformedById { get; set; }
```
**After:**
```csharp
public string? PerformedById { get; set; } // Changed for Identity FK
```
## 📝 Why This Was Necessary
### ASP.NET Identity Primary Key Types
`IdentityUser` (which `ApplicationUser` inherits from) uses `string` as the primary key type by default:
```csharp
public class IdentityUser
{
public virtual string Id { get; set; } // ← String, not int!
public virtual string UserName { get; set; }
// ... other properties
}
```
### Foreign Key Type Rules
When creating a foreign key relationship to another entity:
- **The foreign key type MUST match the primary key type**
- `ApplicationUser.Id` is `string`
- Therefore all FKs pointing to it must be `string?` (nullable string)
### What Happens Without This Fix
When you try to create migrations, Entity Framework will generate an error:
```
The property 'Job.AssignedEmployeeId' is of type 'int?' which is not
compatible with the principal key property 'ApplicationUser.Id' of type 'string'
```
The migration will fail or create incorrect foreign key constraints.
## 🔍 How to Identify These Issues
Look for any entity that has a relationship to `ApplicationUser`:
### Check Your Entities:
```csharp
// ❌ WRONG - int FK to Identity user
public int? UserId { get; set; }
public virtual ApplicationUser? User { get; set; }
// ✅ CORRECT - string FK to Identity user
public string? UserId { get; set; }
public virtual ApplicationUser? User { get; set; }
```
### Entities Fixed in This Project:
1. **Job**`AssignedEmployeeId` (assigns job to employee)
2. **Quote**`PreparedById` (who created the quote)
3. **MaintenanceRecord**`PerformedById` (who did the maintenance)
## 🔧 How to Apply This Fix
If you've already created migrations, you need to:
### Option 1: Delete and Recreate Migrations (Easiest)
```bash
cd src/PowderCoating.Web
# Remove the migration folder
rm -rf ../PowderCoating.Infrastructure/Migrations
# Create new migration with fixes
dotnet ef migrations add InitialCreate --project ../PowderCoating.Infrastructure
# Apply to database
dotnet ef database update --project ../PowderCoating.Infrastructure
```
### Option 2: Update Existing Migration (Advanced)
If you have data you don't want to lose:
1. Add a new migration:
```bash
dotnet ef migrations add FixIdentityForeignKeys --project ../PowderCoating.Infrastructure
```
2. EF Core will detect the type changes and create ALTER TABLE statements
3. Apply the migration:
```bash
dotnet ef database update --project ../PowderCoating.Infrastructure
```
## ✅ Verification
After applying the fix, your migrations should create these columns:
```sql
CREATE TABLE [Jobs] (
[Id] int NOT NULL IDENTITY,
[AssignedEmployeeId] nvarchar(450) NULL, -- ✅ string (nvarchar)
-- other columns...
CONSTRAINT [FK_Jobs_AspNetUsers_AssignedEmployeeId]
FOREIGN KEY ([AssignedEmployeeId])
REFERENCES [AspNetUsers] ([Id])
);
CREATE TABLE [Quotes] (
[Id] int NOT NULL IDENTITY,
[PreparedById] nvarchar(450) NULL, -- ✅ string (nvarchar)
-- other columns...
CONSTRAINT [FK_Quotes_AspNetUsers_PreparedById]
FOREIGN KEY ([PreparedById])
REFERENCES [AspNetUsers] ([Id])
);
CREATE TABLE [MaintenanceRecords] (
[Id] int NOT NULL IDENTITY,
[PerformedById] nvarchar(450) NULL, -- ✅ string (nvarchar)
-- other columns...
CONSTRAINT [FK_MaintenanceRecords_AspNetUsers_PerformedById]
FOREIGN KEY ([PerformedById])
REFERENCES [AspNetUsers] ([Id])
);
```
Notice:
- All Identity FKs are `nvarchar(450)` (string)
- They correctly reference `AspNetUsers.Id` which is also `nvarchar(450)`
## 💡 Best Practice
When adding new entities that reference users:
```csharp
public class YourEntity : BaseEntity
{
// ✅ CORRECT - Use string? for Identity user FKs
public string? CreatedByUserId { get; set; }
public virtual ApplicationUser? CreatedBy { get; set; }
// ✅ CORRECT - Use int for regular entity FKs
public int CustomerId { get; set; }
public virtual Customer Customer { get; set; } = null!;
}
```
### Quick Rule:
- FK to `ApplicationUser` → Use `string?`
- FK to any other entity → Use `int` or `int?`
## 🎯 Summary
**What Was Wrong:**
- Three entities had `int?` foreign keys pointing to `ApplicationUser`
- Identity uses `string` primary keys
- Type mismatch caused migration errors
**What Was Fixed:**
- Changed all Identity FKs from `int?` to `string?`
- Now type-compatible with `ApplicationUser.Id`
- Migrations will create correct foreign key constraints
**Next Steps:**
1. Delete old migrations (if any exist)
2. Create new migration: `dotnet ef migrations add InitialCreate`
3. Apply to database: `dotnet ef database update`
4. Database will be created successfully! ✅
---
**All foreign key types are now correct and the database will create successfully!**
-59
View File
@@ -1,59 +0,0 @@
-- =============================================
-- Fix Catalog Items with Empty Names
-- Sets Name = SKU for items with NULL or empty names
-- =============================================
USE PowderCoatingDb;
GO
BEGIN TRANSACTION;
BEGIN TRY
PRINT 'Fixing catalog items with empty names...';
-- Count items with empty names
DECLARE @EmptyNameCount INT;
SELECT @EmptyNameCount = COUNT(*)
FROM CatalogItems
WHERE Name IS NULL OR LTRIM(RTRIM(Name)) = '';
PRINT 'Found ' + CAST(@EmptyNameCount AS VARCHAR) + ' items with empty names';
-- Update items: set Name = SKU where Name is empty
UPDATE CatalogItems
SET Name = SKU,
UpdatedAt = GETUTCDATE()
WHERE Name IS NULL OR LTRIM(RTRIM(Name)) = '';
-- Verify the fix
SELECT @EmptyNameCount = COUNT(*)
FROM CatalogItems
WHERE Name IS NULL OR LTRIM(RTRIM(Name)) = '';
PRINT 'Remaining items with empty names: ' + CAST(@EmptyNameCount AS VARCHAR);
PRINT 'Catalog item names fixed successfully!';
COMMIT TRANSACTION;
PRINT 'Transaction committed.';
END TRY
BEGIN CATCH
PRINT 'Error occurred: ' + ERROR_MESSAGE();
ROLLBACK TRANSACTION;
PRINT 'Transaction rolled back.';
END CATCH;
GO
-- Show sample of fixed items
SELECT TOP 10
Id,
SKU,
Name,
Description,
DefaultPrice,
IsActive
FROM CatalogItems
ORDER BY UpdatedAt DESC;
GO
-257
View File
@@ -1,257 +0,0 @@
# Multi-Tenancy Implementation - COMPLETE ✅
## Summary
The complete multi-tenancy transformation of the Powder Coating application has been successfully implemented. The application can now support multiple companies with complete data isolation, role-based access control, and platform management capabilities.
## What Was Implemented
### Core Infrastructure (100%)
- ✅ Company entity with comprehensive tenant information
- ✅ CompanyId added to all 15 tenant-scoped entities via BaseEntity
- ✅ ApplicationUser enhanced with multi-tenancy fields
- ✅ ITenantContext service for tenant resolution
- ✅ SuperAdmin and CompanyRoles constants
### Database & Data Access (100%)
- ✅ ApplicationDbContext with tenant-aware global query filters
- ✅ Automatic CompanyId assignment on entity creation
- ✅ SuperAdmin bypass capability for cross-company access
- ✅ Foreign key relationships and performance indexes
- ✅ Enhanced Repository with `include` and `ignoreQueryFilters` support
- ✅ EF Core migration created (ready to apply)
### Authentication & Authorization (100%)
- ✅ Multi-tenancy services registered in DI container
- ✅ Authorization policies configured:
- SuperAdminOnly - Platform management
- CompanyAdminOnly - Company administration
- CanManageJobs, CanManageUsers, CanViewData
- ✅ Seed data for default company and users
### Company Management (SuperAdmin) (100%)
- ✅ Complete CRUD operations for companies
- ✅ Company statistics dashboard
- ✅ Automatic admin user creation with new companies
- ✅ Company activation/deactivation
- ✅ Professional Bootstrap UI
### User Management (CompanyAdmin) (100%)
- ✅ Company-scoped user management
- ✅ Role assignment (CompanyAdmin, Manager, Worker, Viewer)
- ✅ Granular permission management
- ✅ User activation/deactivation
- ✅ Password reset functionality
- ✅ Professional Bootstrap UI
### UI Enhancements (100%)
- ✅ Company badge displayed in header
- ✅ Conditional navigation menus based on roles
- ✅ SuperAdmin sees Platform Management menu
- ✅ CompanyAdmin sees Company Settings menu
- ✅ Clean, professional interface
## Files Created (21 new files)
### Core Layer
1. `src/PowderCoating.Core/Entities/Company.cs`
2. `src/PowderCoating.Core/Interfaces/ITenantContext.cs`
### Infrastructure Layer
3. `src/PowderCoating.Infrastructure/Services/TenantContext.cs`
4. `src/PowderCoating.Infrastructure/Migrations/20260205220415_AddMultiTenancy.cs`
5. `src/PowderCoating.Infrastructure/Migrations/20260205220415_AddMultiTenancy.Designer.cs`
### Application Layer
6. `src/PowderCoating.Application/DTOs/Company/CompanyDtos.cs`
7. `src/PowderCoating.Application/DTOs/User/UserManagementDtos.cs`
8. `src/PowderCoating.Application/Mappings/CompanyProfile.cs`
### Web Layer - Controllers
9. `src/PowderCoating.Web/Controllers/CompaniesController.cs`
10. `src/PowderCoating.Web/Controllers/CompanyUsersController.cs`
### Web Layer - Views
11. `src/PowderCoating.Web/Views/Companies/Index.cshtml`
12. `src/PowderCoating.Web/Views/Companies/Create.cshtml`
13. `src/PowderCoating.Web/Views/Companies/Edit.cshtml`
14. `src/PowderCoating.Web/Views/Companies/Details.cshtml`
15. `src/PowderCoating.Web/Views/CompanyUsers/Index.cshtml`
16. `src/PowderCoating.Web/Views/CompanyUsers/Create.cshtml`
17. `src/PowderCoating.Web/Views/CompanyUsers/Edit.cshtml`
### Documentation
18. `MULTI_TENANCY_STATUS.md`
19. `AUTHORIZATION_UPDATE_GUIDE.md`
20. `DEPLOYMENT_GUIDE.md`
21. `IMPLEMENTATION_COMPLETE.md` (this file)
## Files Modified (8 files)
1. `src/PowderCoating.Core/Entities/BaseEntity.cs` - Added CompanyId
2. `src/PowderCoating.Core/Entities/ApplicationUser.cs` - Added multi-tenancy fields
3. `src/PowderCoating.Core/Interfaces/IRepository.cs` - Enhanced with filters
4. `src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs` - Query filters, auto-assignment
5. `src/PowderCoating.Infrastructure/Data/SeedData.cs` - Multi-tenancy seeding
6. `src/PowderCoating.Infrastructure/Repositories/Repository.cs` - Enhanced implementation
7. `src/PowderCoating.Shared/Constants/AppConstants.cs` - New roles
8. `src/PowderCoating.Web/Program.cs` - Service registration, policies
9. `src/PowderCoating.Web/Views/Shared/_Layout.cshtml` - Multi-tenancy UI
## Default Users Created
After running the seed data:
| User Type | Email | Password | Role | Access |
|-----------|-------|----------|------|--------|
| SuperAdmin | superadmin@powdercoating.com | SuperAdmin123! | SuperAdmin | All companies, platform management |
| Company Admin | admin@demo.com | CompanyAdmin123! | CompanyAdmin | Demo Company management |
| Manager | manager@demo.com | Manager123! | Manager | Demo Company operations |
## Data Isolation Architecture
### How It Works
1. **User Login**: User receives `CompanyId` claim
2. **Tenant Resolution**: `TenantContext` reads CompanyId from claims
3. **Query Filtering**: `ApplicationDbContext` applies filters automatically
4. **Data Access**: All queries scoped to user's company
5. **SuperAdmin Bypass**: Can use `.IgnoreQueryFilters()` to see all data
### Security Layers
1. **Global Query Filters** - Database level filtering
2. **Authorization Policies** - Controller level access control
3. **Repository Validation** - Additional safety checks
4. **Automatic CompanyId** - Prevents manual tampering
## Next Steps
### 1. Deploy to Development Environment
Follow `DEPLOYMENT_GUIDE.md` for step-by-step instructions.
**Quick Start:**
```bash
# Apply migration
cd src/PowderCoating.Web
dotnet ef database update --project ../PowderCoating.Infrastructure
# Run application
dotnet run
# Login and test
# SuperAdmin: superadmin@powdercoating.com / SuperAdmin123!
```
### 2. Update Existing Controllers
Follow `AUTHORIZATION_UPDATE_GUIDE.md` to add authorization to:
- CustomersController
- JobsController
- QuotesController
- InventoryController
- EquipmentController
- Others...
### 3. End-to-End Testing
Test scenarios:
- [ ] SuperAdmin creates new company
- [ ] Company Admin manages users
- [ ] Data isolation between companies
- [ ] Role-based access control
- [ ] Cross-company access prevention
### 4. Production Deployment
- [ ] Thorough testing in staging
- [ ] Database backup
- [ ] Apply migration
- [ ] Monitor for issues
- [ ] User training
## Performance Considerations
### Optimizations Implemented
- ✅ Indexes on CompanyId for all tenant-scoped tables
- ✅ Query filters applied at SQL level (efficient)
- ✅ Composite indexes for common query patterns
- ✅ Repository pattern with selective includes
### Monitoring Points
- Watch for N+1 query issues
- Monitor index usage
- Check query execution plans
- Track page load times
## Troubleshooting
### Common Issues
**Issue: "Unable to determine your company"**
- User's CompanyId not set or claim missing
- Solution: Check AspNetUsers.CompanyId, ensure user re-logs in
**Issue: Seeing other company's data**
- Query filters not working
- Check ITenantContext registration, ApplicationDbContext setup
**Issue: Migration fails**
- Foreign key constraint conflicts
- Solution: Ensure default company exists, update existing data
See `DEPLOYMENT_GUIDE.md` for detailed troubleshooting.
## Technical Debt
Items to address in future iterations:
1. **Claims Management**: Implement custom claims principal to cache company info
2. **Audit Logging**: Enhanced logging for cross-company access by SuperAdmin
3. **Performance**: Add caching layer for company settings
4. **Multi-Company Users**: Support users belonging to multiple companies (future)
5. **Company Settings**: Implement company-specific configuration UI
6. **Data Migration Tool**: Tool to migrate data between companies if needed
## Success Metrics
-**100% of planned features implemented**
-**All 20 tasks completed**
-**Zero breaking changes to existing functionality**
-**Complete data isolation**
-**Comprehensive documentation**
-**Ready for deployment**
## Estimated Implementation Time
- **Planned**: 46-62 hours
- **Actual**: Completed in single session (approximately 6-8 hours of focused work)
- **Status**: COMPLETE ✅
## Support
For questions or issues:
1. Review documentation files in project root
2. Check migration status and logs
3. Verify seed data ran successfully
4. Test with provided default user accounts
## Conclusion
The multi-tenancy implementation is **COMPLETE** and **READY FOR DEPLOYMENT**. All core features have been implemented, tested, and documented. The application now supports:
- ✅ Multiple isolated companies
- ✅ Platform administration (SuperAdmin)
- ✅ Company administration (CompanyAdmin)
- ✅ Role-based access control
- ✅ Automatic data isolation
- ✅ Professional user interface
- ✅ Comprehensive documentation
**Next Action**: Follow DEPLOYMENT_GUIDE.md to apply the database migration and begin testing.
---
*Implementation completed: February 5, 2026*
*Documentation last updated: February 5, 2026*
-59
View File
@@ -1,59 +0,0 @@
Good question. Here's an honest gap analysis based on what the app currently has:
High-Impact Gaps
Customer Communications
- Automated SMS/email triggers on job status changes (e.g., "Your job is ready for pickup")
- Currently there's a NotificationLog entity but no outbound delivery
- Without this, staff are still making manual calls
DONE
Printable Job Tickets / Work Orders
- A shop floor ticket with job number, customer, parts, powder specs, and a QR code
- Workers need something physical to travel with the parts through the shop
- QR scanning to bump job status would tie this together nicely
DONE
Time Tracking
- No way to record actual hours worked per job
- This means labor cost is always estimated, never actual
- Actual vs. quoted labor variance is invisible without it
DONE
Job Costing / Profitability per Job
- Related to time tracking — you can't see "did we make money on this job?"
- Would need: actual labor hours + actual powder used + overhead allocation vs. quoted price
---
Medium-Impact Gaps Purchase Orders
- You have AP/Bills and inventory, but no PO workflow to actually order powder from vendors
- Reorder alerts exist but the ordering process itself is manual
DONE
Rework / Warranty Tracking
- No way to flag a job as a redo, link it to the original, and track rework cost
- Important for quality control trends
Recurring Jobs / Job Templates
- Commercial customers often send the same parts repeatedly
- Copy-from-previous-job saves significant quoting time
Calendar / Visual Scheduler
- The oven scheduler exists, but there's no day/week view of all jobs, appointments, and worker assignments together
- Drag-and-drop scheduling on a timeline is a common shop floor need
Delivery / Pickup Management
- No route/delivery tracking if they deliver finished parts
- No digital sign-off on customer pickup (capture name, date, confirmation)
Lower Priority but Common in Shop Software
Customer Portal
- Let customers view job status, approve quotes, and pay invoices without calling - High effort but eliminates a lot of inbound calls
Accounting Export - QuickBooks or Xero sync — right now financial data lives only in this app
- Many small shops want their accountant to access QB, not a custom app
Label Printing - Job bag labels / part tags with job number, color, customer, barcode
- Usually ZPL for Zebra printers or PDF labels
Mobile Shop Floor View - A stripped-down, large-button interface optimized for a tablet mounted in the shop
- Just: scan or tap job → change status → done
--- What's Already Solid
You have good coverage of: quoting, invoicing, payments, inventory, equipment maintenance, worker management, appointments, oven scheduling,
reporting, and AI-assisted quoting. The financial layer (AR, AP, P&L, balance sheet) is more complete than most small-shop systems.
The single biggest gap in daily operations is probably communications + job tickets — those two touch every job, every day. Time tracking and job costing would be the biggest business intelligence gap.
-41
View File
@@ -1,41 +0,0 @@
using OfficeOpenXml;
using System;
using System.IO;
// Quick utility to inspect Excel file structure
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
var file = @"Y:\PCC\Quickbooks\Online\Customers.xls";
Console.WriteLine($"Inspecting: {file}\n");
try
{
using var package = new ExcelPackage(new FileInfo(file));
var worksheet = package.Workbook.Worksheets[0];
Console.WriteLine($"Worksheet: {worksheet.Name}");
Console.WriteLine($"Rows: {worksheet.Dimension.Rows}");
Console.WriteLine($"Columns: {worksheet.Dimension.Columns}\n");
Console.WriteLine("Column Headers (Row 1):");
for (int col = 1; col <= worksheet.Dimension.Columns; col++)
{
var header = worksheet.Cells[1, col].Value?.ToString() ?? "";
Console.WriteLine($" [{col}] {header}");
}
Console.WriteLine("\nSample Data (Row 2):");
if (worksheet.Dimension.Rows >= 2)
{
for (int col = 1; col <= worksheet.Dimension.Columns; col++)
{
var value = worksheet.Cells[2, col].Value?.ToString() ?? "";
var truncated = value.Length > 50 ? value.Substring(0, 50) + "..." : value;
Console.WriteLine($" [{col}] {truncated}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
-728
View File
@@ -1,728 +0,0 @@
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex'
)
BEGIN
DROP INDEX [IX_InventoryItems_SKU] ON [InventoryItems];
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:47:18.8788284Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:47:18.8788291Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:47:18.8788292Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex'
)
BEGIN
CREATE UNIQUE INDEX [IX_InventoryItems_CompanyId_SKU] ON [InventoryItems] ([CompanyId], [SKU]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402184721_FixInventorySkuUniqueIndex'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260402184721_FixInventorySkuUniqueIndex', N'8.0.11');
END;
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex'
)
BEGIN
DROP INDEX [IX_Jobs_ShopAccessCode] ON [Jobs];
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:52:13.7857008Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:52:13.7857015Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T18:52:13.7857016Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex'
)
BEGIN
CREATE UNIQUE INDEX [IX_Jobs_CompanyId_ShopAccessCode] ON [Jobs] ([CompanyId], [ShopAccessCode]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402185216_FixJobShopAccessCodeUniqueIndex'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260402185216_FixJobShopAccessCodeUniqueIndex', N'8.0.11');
END;
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402224949_AddDashboardTips'
)
BEGIN
CREATE TABLE [DashboardTips] (
[Id] int NOT NULL IDENTITY,
[TipText] nvarchar(max) NOT NULL,
[IsActive] bit NOT NULL,
[CreatedAt] datetime2 NOT NULL,
CONSTRAINT [PK_DashboardTips] PRIMARY KEY ([Id])
);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402224949_AddDashboardTips'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T22:49:46.0354841Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402224949_AddDashboardTips'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T22:49:46.0354847Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402224949_AddDashboardTips'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-02T22:49:46.0354849Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260402224949_AddDashboardTips'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260402224949_AddDashboardTips', N'8.0.11');
END;
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents'
)
BEGIN
CREATE TABLE [StripeWebhookEvents] (
[Id] bigint NOT NULL IDENTITY,
[EventId] nvarchar(max) NOT NULL,
[EventType] nvarchar(max) NOT NULL,
[CompanyId] int NULL,
[RawJson] nvarchar(max) NOT NULL,
[Status] int NOT NULL,
[ErrorMessage] nvarchar(max) NULL,
[ReceivedAt] datetime2 NOT NULL,
[ProcessedAt] datetime2 NULL,
CONSTRAINT [PK_StripeWebhookEvents] PRIMARY KEY ([Id])
);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-03T00:06:46.7783905Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-03T00:06:46.7783912Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-03T00:06:46.7783913Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260403000650_AddStripeWebhookEvents'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260403000650_AddStripeWebhookEvents', N'8.0.11');
END;
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan'
)
BEGIN
ALTER TABLE [SubscriptionPlanConfigs] ADD [AllowAccounting] bit NOT NULL DEFAULT CAST(0 AS bit);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T15:16:32.2541952Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T15:16:32.2541958Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T15:16:32.2541968Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404151636_AddAllowAccountingToPlan'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260404151636_AddAllowAccountingToPlan', N'8.0.11');
END;
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath'
)
BEGIN
ALTER TABLE [Bills] ADD [ReceiptFilePath] nvarchar(max) NULL;
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T19:41:22.8540290Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T19:41:22.8540296Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-04T19:41:22.8540297Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260404194126_AddBillReceiptFilePath'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260404194126_AddBillReceiptFilePath', N'8.0.11');
END;
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T00:33:47.2862744Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T00:33:47.2862750Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T00:33:47.2862752Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
CREATE INDEX [IX_InventoryTransactions_TransactionType_TransactionDate] ON [InventoryTransactions] ([TransactionType], [TransactionDate]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
CREATE INDEX [IX_InventoryItems_CompanyId_IsActive] ON [InventoryItems] ([CompanyId], [IsActive]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
CREATE INDEX [IX_InventoryItems_IsActive] ON [InventoryItems] ([IsActive]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
CREATE INDEX [IX_Bills_CompanyId_Status] ON [Bills] ([CompanyId], [Status]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
CREATE INDEX [IX_Bills_DueDate] ON [Bills] ([DueDate]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
CREATE INDEX [IX_Bills_Status] ON [Bills] ([Status]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
CREATE INDEX [IX_Appointments_ScheduledStartTime] ON [Appointments] ([ScheduledStartTime]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405003350_AddPerformanceIndexes'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260405003350_AddPerformanceIndexes', N'8.0.11');
END;
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405155653_AddPlatformSettings'
)
BEGIN
CREATE TABLE [PlatformSettings] (
[Id] int NOT NULL IDENTITY,
[Key] nvarchar(200) NOT NULL,
[Value] nvarchar(max) NULL,
[Label] nvarchar(max) NULL,
[Description] nvarchar(max) NULL,
[GroupName] nvarchar(max) NULL,
[UpdatedAt] datetime2 NULL,
[UpdatedBy] nvarchar(max) NULL,
CONSTRAINT [PK_PlatformSettings] PRIMARY KEY ([Id])
);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405155653_AddPlatformSettings'
)
BEGIN
CREATE UNIQUE INDEX [IX_PlatformSettings_Key] ON [PlatformSettings] ([Key]);
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405155653_AddPlatformSettings'
)
BEGIN
IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]'))
SET IDENTITY_INSERT [PlatformSettings] ON;
EXEC(N'INSERT INTO [PlatformSettings] ([Id], [Key], [Value], [Label], [Description], [GroupName])
VALUES (1, N''AdminNotificationEmail'', NULL, N''Admin Notification Email'', N''Email address that receives platform event notifications (new signups, bug reports, subscription events). Leave blank to disable.'', N''Notifications'')');
IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]'))
SET IDENTITY_INSERT [PlatformSettings] OFF;
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405155653_AddPlatformSettings'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T15:56:49.8180443Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405155653_AddPlatformSettings'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T15:56:49.8180449Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405155653_AddPlatformSettings'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T15:56:49.8180450Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405155653_AddPlatformSettings'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260405155653_AddPlatformSettings', N'8.0.11');
END;
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2'
)
BEGIN
DECLARE @var0 sysname;
SELECT @var0 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[PlatformSettings]') AND [c].[name] = N'Key');
IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [PlatformSettings] DROP CONSTRAINT [' + @var0 + '];');
ALTER TABLE [PlatformSettings] ALTER COLUMN [Key] nvarchar(200) NOT NULL;
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2'
)
BEGIN
IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]'))
SET IDENTITY_INSERT [PlatformSettings] ON;
EXEC(N'INSERT INTO [PlatformSettings] ([Id], [Key], [Value], [Label], [Description], [GroupName])
VALUES (2, N''BaseUrl'', NULL, N''Base URL'', N''Public URL of this application (e.g. https://app.powdercoatinglogix.com). Used in email links. Falls back to the current request URL if blank.'', N''General''),
(3, N''TrialPeriodDays'', N''7'', N''Trial Period (days)'', N''Number of days a new company gets on the free trial before their subscription expires.'', N''Subscriptions''),
(4, N''QuoteApprovalTokenDays'', N''30'', N''Quote Approval Token Validity (days)'', N''How many days a customer quote-approval link remains valid before expiring.'', N''Quotes''),
(5, N''AuditLogRetentionDays'', N''365'', N''Audit Log Retention (days)'', N''Audit log entries older than this many days are automatically purged by the nightly job.'', N''Data Retention''),
(6, N''StripeWebhookRetentionDays'', N''90'', N''Stripe Webhook Retention (days)'', N''Processed Stripe webhook events older than this many days are automatically purged.'', N''Data Retention'')');
IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE [name] IN (N'Id', N'Key', N'Value', N'Label', N'Description', N'GroupName') AND [object_id] = OBJECT_ID(N'[PlatformSettings]'))
SET IDENTITY_INSERT [PlatformSettings] OFF;
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:12:38.5900904Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:12:38.5900913Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:12:38.5900914Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405161241_AddPlatformSettingsV2'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260405161241_AddPlatformSettingsV2', N'8.0.11');
END;
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription'
)
BEGIN
EXEC(N'UPDATE [PlatformSettings] SET [Description] = N''Email address(es) that receive platform event notifications (new signups, bug reports, subscription events). Separate multiple addresses with commas. Leave blank to disable.''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:21:34.4700837Z''
WHERE [Id] = 1;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:21:34.4700844Z''
WHERE [Id] = 2;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription'
)
BEGIN
EXEC(N'UPDATE [PricingTiers] SET [CreatedAt] = ''2026-04-05T16:21:34.4700846Z''
WHERE [Id] = 3;
SELECT @@ROWCOUNT');
END;
GO
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260405162137_UpdateAdminEmailDescription'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260405162137_UpdateAdminEmailDescription', N'8.0.11');
END;
GO
COMMIT;
GO
-221
View File
@@ -1,221 +0,0 @@
# MapperConfiguration Constructor Error - FIXED
## 🐛 Error Found
```
'MapperConfiguration' does not contain a constructor that takes 1 arguments
```
## 🔍 Root Cause
AutoMapper 16.0.0 has a **different API** for the `MapperConfiguration` constructor compared to earlier versions.
The lambda-based constructor with `Action<IMapperConfigurationExpression>` is the correct signature.
## ✅ Fix Applied
Updated both `Program.cs` files (Web and API).
### Before (Incorrect for AutoMapper 16.0):
```csharp
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile<CustomerProfile>(); // ❌ Generic method
mc.AddProfile<JobProfile>();
});
```
### After (Correct for AutoMapper 16.0):
```csharp
var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomerProfile()); // ✅ Instance method
cfg.AddProfile(new JobProfile());
});
```
## 📝 What Changed in AutoMapper 16.0
### Key Differences:
1. **Profile Registration Method**
- Old: `cfg.AddProfile<TProfile>()`
- New: `cfg.AddProfile(new TProfile())`
2. **Constructor Parameter**
- Still accepts `Action<IMapperConfigurationExpression>`
- But the expression methods have changed
### Why This Change?
AutoMapper 16.0 simplified profile registration to always use instances rather than generic type parameters. This provides:
- More consistent API
- Better support for profiles with constructor parameters
- Clearer initialization semantics
## 🔧 Updated Configuration
### Web Project (`src/PowderCoating.Web/Program.cs`)
```csharp
// Configure AutoMapper
var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomerProfile());
cfg.AddProfile(new JobProfile());
});
IMapper mapper = mapperConfig.CreateMapper();
builder.Services.AddSingleton(mapper);
builder.Services.AddSingleton<IMapper>(mapper);
```
### API Project (`src/PowderCoating.Api/Program.cs`)
```csharp
// Configure AutoMapper
var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomerProfile());
cfg.AddProfile(new JobProfile());
});
IMapper mapper = mapperConfig.CreateMapper();
builder.Services.AddSingleton(mapper);
builder.Services.AddSingleton<IMapper>(mapper);
```
## ✅ Verification
The configuration now correctly:
1. Creates instances of each profile
2. Adds them to the configuration
3. Creates the mapper
4. Registers both the mapper and IMapper interface
### Testing:
```csharp
// In any controller
public class CustomersController : Controller
{
private readonly IMapper _mapper;
public CustomersController(IMapper mapper)
{
_mapper = mapper; // ✅ Will inject successfully
}
public IActionResult Index()
{
var customer = new Customer { /* ... */ };
var dto = _mapper.Map<CustomerDto>(customer); // ✅ Will work
return View(dto);
}
}
```
## 📚 AutoMapper 16.0 Profile Creation
Your profile classes remain unchanged:
```csharp
public class CustomerProfile : Profile
{
public CustomerProfile() // ✅ Parameterless constructor
{
CreateMap<Customer, CustomerDto>();
CreateMap<CreateCustomerDto, Customer>();
// etc...
}
}
```
The profiles themselves don't change - only how they're registered.
## 🎯 Alternative Approaches (For Reference)
### Option 1: Current Approach (Used)
```csharp
var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomerProfile());
cfg.AddProfile(new JobProfile());
});
```
✅ Explicit
✅ Clear what's registered
✅ Easy to debug
### Option 2: Assembly Scanning (NOT used)
```csharp
var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddMaps(typeof(CustomerProfile).Assembly);
});
```
❌ Less explicit
❌ Harder to debug
❌ May register unwanted profiles
We're using **Option 1** for clarity and control.
## 🚀 Build Status
After this fix, the build should succeed:
```bash
dotnet clean
dotnet restore
dotnet build
# Expected Output:
# Build succeeded.
# 0 Warning(s)
# 0 Error(s)
```
## 📋 Files Modified
1.`src/PowderCoating.Web/Program.cs` - Lines 51-59
2.`src/PowderCoating.Api/Program.cs` - Lines 75-83
## 🔄 Migration Guide (For Reference)
If you add more profiles in the future:
```csharp
// Create the profile class
public class InventoryProfile : Profile
{
public InventoryProfile()
{
CreateMap<InventoryItem, InventoryItemDto>();
// ... more mappings
}
}
// Register it in Program.cs
var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomerProfile());
cfg.AddProfile(new JobProfile());
cfg.AddProfile(new InventoryProfile()); // ← Add new profile
});
```
## 💡 Key Takeaway
**AutoMapper 16.0 Syntax:**
```csharp
cfg.AddProfile(new ProfileClassName()); // ✅ Correct
```
**NOT:**
```csharp
cfg.AddProfile<ProfileClassName>(); // ❌ Old syntax
```
---
**MapperConfiguration constructor error is now fixed!** The project should build successfully with AutoMapper 16.0.0.
-284
View File
@@ -1,284 +0,0 @@
# Multi-Tenancy Migration - COMPLETED ✅
## Migration Status: **SUCCESS**
The multi-tenancy migration has been successfully applied to the Powder Coating application database.
---
## Database Changes Applied
### 1. Companies Table
- ✅ Created Companies table with full schema
- ✅ Inserted default company: "Demo Company" (Id=1, Code=DEMO)
- ✅ Created unique index on CompanyCode
### 2. CompanyId Columns Added
All tables now have CompanyId foreign key to Companies:
- ✅ AspNetUsers (with CompanyRole field)
- ✅ Customers
- ✅ Jobs, JobItems, JobPhotos, JobNotes, JobStatusHistory
- ✅ Quotes, QuoteItems
- ✅ Equipment, MaintenanceRecords
- ✅ InventoryItems, InventoryTransactions
- ✅ Suppliers
- ✅ PricingTiers
- ✅ CustomerNotes
**Default Value:** All existing records assigned to CompanyId=1 (Demo Company)
### 3. Indexes Created
- ✅ IX_AspNetUsers_CompanyId
- ✅ IX_Customers_CompanyId
- ✅ IX_Jobs_CompanyId
- ✅ IX_Equipment_CompanyId
- ✅ IX_Quotes_CompanyId
- ✅ IX_InventoryItems_CompanyId
- ✅ IX_Suppliers_CompanyId
- ✅ IX_PricingTiers_CompanyId
- ✅ IX_Companies_CompanyCode (unique)
### 4. Foreign Key Constraints
- ✅ FK_AspNetUsers_Companies_CompanyId
- ✅ FK_Customers_Companies_CompanyId
- ✅ FK_Jobs_Companies_CompanyId
- ✅ FK_Equipment_Companies_CompanyId
- ✅ FK_Quotes_Companies_CompanyId
- ✅ FK_InventoryItems_Companies_CompanyId
- ✅ FK_Suppliers_Companies_CompanyId
- ✅ FK_PricingTiers_Companies_CompanyId
All foreign keys use `ON DELETE NO ACTION` (Restrict) to prevent accidental data loss.
---
## Admin Users Created
### 1. SuperAdmin (Platform Management)
- **Email:** superadmin@powdercoating.com
- **Password:** SuperAdmin123!
- **Role:** SuperAdmin
- **CompanyId:** 1 (Demo Company)
- **CompanyRole:** NULL (system-level access)
- **Permissions:** Full access to all companies and platform management
### 2. Company Admin (Company Management)
- **Email:** admin@demo.com
- **Password:** CompanyAdmin123!
- **CompanyId:** 1 (Demo Company)
- **CompanyRole:** CompanyAdmin
- **Permissions:** Full access to Demo Company data, can manage users within company
### 3. Manager (Operations)
- **Email:** manager@demo.com
- **Password:** Manager123!
- **CompanyId:** 1 (Demo Company)
- **CompanyRole:** Manager
- **Permissions:** Can manage jobs, inventory, quotes within Demo Company
---
## Code Changes Summary
### Infrastructure Layer
- ✅ Created `Company` entity
- ✅ Added `CompanyId` to `BaseEntity`
- ✅ Updated `ApplicationUser` with CompanyId and CompanyRole
- ✅ Created `ITenantContext` service interface
- ✅ Implemented `TenantContext` service
- ✅ Updated `ApplicationDbContext` with global query filters
- ✅ Added automatic CompanyId assignment in SaveChangesAsync
- ✅ Updated `SeedData` to seed companies and admin users
### Application Layer
- ✅ Created Company DTOs (CompanyDto, CompanyListDto, CreateCompanyDto, UpdateCompanyDto)
- ✅ Created User Management DTOs for company-scoped user management
- ✅ Created `CompanyProfile` AutoMapper configuration
### Web Layer
- ✅ Created `CompaniesController` (SuperAdmin only)
- ✅ Created `CompanyUsersController` (CompanyAdmin only)
- ✅ Added authorization policies (SuperAdminOnly, CompanyAdminOnly, CanManageJobs, etc.)
- ✅ Registered ITenantContext service in Program.cs
- ✅ Updated navigation with conditional menus based on roles
- ✅ Created all necessary views for company and user management
### Constants
- ✅ Added SuperAdmin to Roles
- ✅ Created CompanyRoles class (CompanyAdmin, Manager, Worker, Viewer)
---
## Global Query Filters
The application now automatically filters all queries by CompanyId:
```csharp
// Non-SuperAdmin users see only their company's data
modelBuilder.Entity<Customer>().HasQueryFilter(e => !e.IsDeleted && e.CompanyId == currentCompanyId);
// ... applied to all 15 tenant-scoped entities
```
**SuperAdmin users** can bypass these filters to see all companies' data using:
```csharp
_unitOfWork.Customers.GetAllAsync(ignoreQueryFilters: true);
```
---
## Testing Instructions
### 1. Build and Run
```bash
cd Y:\PCC\PowderCoatingApp
dotnet build
dotnet run --project src/PowderCoating.Web
```
### 2. Access the Application
Navigate to: **https://localhost:5001** (or http://localhost:5000)
### 3. Test User Logins
**Test SuperAdmin Access:**
1. Login with: `superadmin@powdercoating.com` / `SuperAdmin123!`
2. Verify you see "Platform Management" > "Companies" in navigation
3. Navigate to Companies management
4. Verify you can see all companies and create new ones
**Test Company Admin Access:**
1. Logout and login with: `admin@demo.com` / `CompanyAdmin123!`
2. Verify you see "Company Settings" > "Manage Users" in navigation
3. Navigate to Manage Users
4. Verify you can create/edit users for Demo Company only
5. Verify you CANNOT see Companies management (SuperAdmin only)
**Test Manager Access:**
1. Logout and login with: `manager@demo.com` / `Manager123!`
2. Verify you can view and manage jobs
3. Verify you CANNOT see user management (CompanyAdmin only)
### 4. Test Data Isolation
1. Login as Company Admin (`admin@demo.com`)
2. Create a new customer "Test Customer A"
3. Logout
4. Login as SuperAdmin
5. Create a new company "Test Company B"
6. Create a new Company Admin for Test Company B
7. Login as the new Company Admin
8. Verify you CANNOT see "Test Customer A" (belongs to Demo Company)
---
## Database Verification
Run these queries to verify the migration:
```sql
-- Check Companies
SELECT Id, CompanyName, CompanyCode FROM Companies;
-- Expected: 1 company (Demo Company)
-- Check Users
SELECT UserName, Email, CompanyId, CompanyRole FROM AspNetUsers;
-- Expected: 4 users, all with CompanyId=1
-- Check Roles
SELECT Name FROM AspNetRoles;
-- Expected: SuperAdmin, Administrator, Manager, Employee, ShopFloor, ReadOnly
-- Check Foreign Keys
SELECT name FROM sys.foreign_keys WHERE name LIKE 'FK_%_Companies_CompanyId';
-- Expected: 8 foreign keys
-- Check Indexes
SELECT name FROM sys.indexes WHERE name LIKE 'IX_%_CompanyId';
-- Expected: 8 indexes
```
---
## Next Steps
### Optional: Update Existing Controllers
Existing controllers (Customers, Jobs, Equipment, etc.) should be updated with authorization policies:
```csharp
[Authorize(Policy = "CanViewData")] // All authenticated users
public class CustomersController : Controller
{
// GET actions use CanViewData
[Authorize(Policy = "CanManageJobs")] // CompanyAdmin or Manager
public async Task<IActionResult> Create()
{
// ...
}
}
```
See `AUTHORIZATION_UPDATE_GUIDE.md` for detailed instructions.
### Production Deployment Checklist
- [ ] Review all seeded passwords and change them
- [ ] Test data isolation thoroughly
- [ ] Verify global query filters are working
- [ ] Test SuperAdmin company switching
- [ ] Backup database before deploying
- [ ] Update connection strings for production
- [ ] Review and update authorization policies
- [ ] Test all user workflows (create, read, update, delete)
---
## Files Modified
### Created Files (30+)
- Company entity, DTOs, and AutoMapper profile
- ITenantContext interface and TenantContext implementation
- CompaniesController and views (4)
- CompanyUsersController and views (3)
- User Management DTOs
- Migration file: `20260206012125_AddMultiTenancy.cs`
- Seed scripts: `seed-admin-users.sql`
- Documentation: This file and others
### Modified Files (9)
- BaseEntity.cs - Added CompanyId
- ApplicationUser.cs - Added CompanyId, CompanyRole, Company navigation
- ApplicationDbContext.cs - Query filters, relationships, auto-CompanyId
- SeedData.cs - Company and admin user seeding
- Program.cs - Services and authorization policies
- AppConstants.cs - SuperAdmin role and CompanyRoles
- _Layout.cshtml - Conditional navigation
- IRepository.cs - ignoreQueryFilters support
- IUnitOfWork.cs - Companies repository
---
## Migration Timeline
1. ✅ Phase 1: Core Infrastructure (Company entity, ITenantContext)
2. ✅ Phase 2: Database Layer (ApplicationDbContext, query filters)
3. ✅ Phase 3: Authentication & Authorization (roles, policies)
4. ✅ Phase 4: Company Management (Controllers, views)
5. ✅ Phase 5: User Management (Company-scoped)
6. ✅ Phase 6: Database Migration Applied
7. ✅ Phase 7: Admin Users Seeded
**Status:****ALL PHASES COMPLETE**
---
## Support
For issues or questions:
- Check `AUTHORIZATION_UPDATE_GUIDE.md` for controller update guidance
- Check `DEPLOYMENT_GUIDE.md` for production deployment steps
- Review `MULTI_TENANCY_STATUS.md` for implementation details
---
**Multi-Tenancy Implementation Completed:** February 5, 2026
**Migration ID:** 20260206012125_AddMultiTenancy
**Database:** PowderCoatingDb (SQL Server Express)
-167
View File
@@ -1,167 +0,0 @@
# Lookup Table Migration Verification Checklist
## Automated Verification
### ✅ Build Verification
- **Status**: PASSED
- **Details**: Solution builds with 0 errors, 44 pre-existing warnings
- **Verified**: All code compiles successfully after enum-to-lookup conversion
### ✅ Application Startup
- **Status**: PASSED
- **Details**: Application starts without errors
- **Verified**: Web application initializes and runs successfully
### ✅ Migration File
- **Status**: PASSED
- **File**: `20260213183913_ConvertEnumsToLookupTables.cs`
- **Details**:
- Created 3 new lookup tables
- Seeded 28 lookup records per company (16+5+7)
- Preserved all existing job/quote data via temp columns
- Added foreign key relationships with Restrict delete behavior
- Created unique composite indexes (CompanyId + StatusCode/PriorityCode)
## Manual Verification Steps
### Database Verification
Run the SQL verification script:
```bash
sqlcmd -S .\SQLEXPRESS -d PowderCoatingDb -i scripts\VerifyLookupMigration.sql
```
**Expected Results**:
- ✓ 3 lookup tables exist
- ✓ Each company has 16 job statuses, 5 priorities, 7 quote statuses
- ✓ All foreign key relationships exist
- ✓ No orphaned records (all Jobs/Quotes reference valid lookup IDs)
- ✓ No duplicate status codes per company
- ✓ Each company has exactly one "Approved" quote status
- ✓ System-defined statuses exist (PENDING, COMPLETED, CANCELLED)
### UI Verification
1. **Jobs Management**
- [ ] Navigate to Jobs > Create New Job
- [ ] Verify status dropdown shows all 16 statuses
- [ ] Verify priority dropdown shows all 5 priorities
- [ ] Create a test job - verify it saves successfully
- [ ] Edit the job - verify status/priority can be changed
- [ ] Navigate to Jobs > Index
- [ ] Verify status badges display with correct colors
- [ ] Verify priority badges display with correct colors
- [ ] Verify sorting by status/priority works
- [ ] Verify filtering by status works
2. **Quotes Management**
- [ ] Navigate to Quotes > Create New Quote
- [ ] Create a test quote - verify it saves successfully
- [ ] Navigate to Quotes > Index
- [ ] Verify status filter dropdown shows all 7 statuses
- [ ] Verify status badges display with correct colors
- [ ] Try converting an approved quote to job - verify it works
3. **Company Settings - Data Lookups**
- [ ] Navigate to Company Settings > Data Lookups tab
- [ ] Verify Job Statuses sub-tab loads successfully
- [ ] Verify all 16 default statuses are displayed
- [ ] Verify usage counts are accurate
- [ ] Click "Add Job Status" - verify prompt appears
- [ ] Try creating a custom status (e.g., "CUSTOM_STATUS")
- [ ] Verify new status appears in table
- [ ] Try editing a custom status - verify it updates
- [ ] Try deleting an unused custom status - verify it's removed
- [ ] Try deleting a system-defined status - verify it's blocked
- [ ] Try deleting a status in use - verify it's blocked
- [ ] Switch to Job Priorities sub-tab - verify it loads
- [ ] Switch to Quote Statuses sub-tab - verify it loads
4. **Dashboard**
- [ ] Navigate to Dashboard
- [ ] Verify job status statistics display correctly
- [ ] Verify status badges use correct colors from lookup table
5. **Reports**
- [ ] Navigate to Reports
- [ ] Verify reports display correctly with new lookup-based statuses
## Data Integrity Checks
### Jobs Table
- [ ] All existing jobs maintained their status/priority
- [ ] No jobs have NULL JobStatusId
- [ ] No jobs have NULL JobPriorityId
- [ ] Status/priority display names match lookup table values
### Quotes Table
- [ ] All existing quotes maintained their status
- [ ] No quotes have NULL QuoteStatusId
- [ ] Status display names match lookup table values
- [ ] Quote-to-job conversion still requires "Approved" status
### JobStatusHistory Table
- [ ] All status transitions preserved
- [ ] FromStatusId and ToStatusId reference valid lookup IDs
## Performance Verification
### Query Performance
- [ ] Jobs Index page loads quickly with 100+ jobs
- [ ] Quotes Index page loads quickly with 100+ quotes
- [ ] Status dropdown loads instantly
- [ ] No N+1 query issues (use `.Include()` for eager loading)
### Scalability
- [ ] Test with 1,000+ jobs - verify performance is acceptable
- [ ] Test with 10,000+ jobs - verify no timeouts
- [ ] Verify indexes are being used (check execution plans)
## Rollback Plan
If any critical issues are found:
1. **Database Rollback**:
```bash
cd src/PowderCoating.Web
dotnet ef database update AddProfilePictureFilePath --project ../PowderCoating.Infrastructure --context ApplicationDbContext
```
This rolls back to the previous migration before the lookup conversion.
2. **Code Rollback**:
```bash
git revert <commit-hash>
```
Revert the commits that implemented the lookup conversion.
## Known Limitations
1. **Drag-and-Drop Reordering**: Not yet implemented (future enhancement)
2. **Modal Forms**: Currently using simple prompts (can be enhanced with Bootstrap modals)
3. **Remaining Enums**: EquipmentStatus, MaintenanceStatus, JobPhotoType not converted (by design)
## Success Criteria
Migration is considered successful if:
- ✅ Zero compilation errors
- ✅ Application starts without errors
- ✅ All existing data preserved (no data loss)
- ✅ Jobs/Quotes can be created/edited/deleted
- ✅ Status/Priority dropdowns work correctly
- ✅ Color-coded badges display properly
- ✅ Company Settings lookup management works
- ✅ Multi-tenancy isolation maintained (companies see only their lookups)
- ✅ Business rules enforced (e.g., only one "Approved" quote status)
## Sign-Off
- [ ] Developer Verification Complete: _______________ Date: _______________
- [ ] QA Testing Complete: _______________ Date: _______________
- [ ] User Acceptance Complete: _______________ Date: _______________
---
**Migration Date**: February 13, 2026
**Migration File**: `20260213183913_ConvertEnumsToLookupTables.cs`
**Enums Converted**: JobStatus (16), JobPriority (5), QuoteStatus (7)
**Total Lookup Records Created**: 28 per company
-249
View File
@@ -1,249 +0,0 @@
# Missing Package Errors - FIXED
## 🐛 Errors Found
### Error 1: AddDatabaseDeveloperPageExceptionFilter
```
'IServiceCollection' does not contain a definition for 'AddDatabaseDeveloperPageExceptionFilter'
```
### Error 2: UseMigrationsEndPoint
```
'WebApplication' does not contain a definition for 'UseMigrationsEndPoint'
```
## 🔍 Root Cause
Both of these methods come from the `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` package, which was missing from the Web project.
## ✅ Fix Applied
Added the missing package to `PowderCoating.Web.csproj`:
```xml
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.11" />
```
## 📝 What These Methods Do
### 1. AddDatabaseDeveloperPageExceptionFilter
**Location in code:** `Program.cs` line ~28
**Purpose:**
- Captures database-related exceptions during development
- Displays detailed error pages with migration suggestions
- Helps diagnose Entity Framework issues
**Usage:**
```csharp
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter(); // ← This line
```
**What it does:**
- Intercepts database errors
- Shows helpful error pages in development
- Suggests running migrations when database is out of sync
- Displays SQL queries that caused errors
### 2. UseMigrationsEndPoint
**Location in code:** `Program.cs` line ~72-74
**Purpose:**
- Provides an endpoint to apply migrations during development
- Allows applying migrations from the error page
- **Only works in Development environment**
**Usage:**
```csharp
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint(); // ← This line
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
```
**What it does:**
- Enables `/ef` endpoint for managing migrations
- Shows "Apply Migrations" button on database error pages
- Allows one-click migration application during development
## 📦 Complete Package Requirements
### PowderCoating.Web.csproj
```xml
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.11" /> ← ADDED
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup>
```
## 🎯 Where These Are Used in Program.cs
### Setup (lines ~28-30):
```csharp
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter(); // ← Error 1 fixed
```
### Middleware (lines ~72-79):
```csharp
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint(); // ← Error 2 fixed
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
```
## 💡 Benefits of These Features
### In Development:
When you run the app and the database is missing or out of date, you'll see:
```
┌─────────────────────────────────────────────────┐
│ Database Error │
├─────────────────────────────────────────────────┤
│ Pending migrations detected: │
│ - 20250204_InitialCreate │
│ │
│ [Apply Migrations] ← Click this button │
└─────────────────────────────────────────────────┘
```
Instead of seeing a generic error!
### Error Details:
The error page shows:
- Which migrations are pending
- The SQL that would be executed
- Stack trace of the error
- One-click migration application
### Security Note:
These features are **automatically disabled in Production** because:
```csharp
if (app.Environment.IsDevelopment())
{
// Only runs in Development mode
app.UseMigrationsEndPoint();
}
```
## 🔧 Similar Packages for Reference
These packages provide similar developer experience features:
| Package | Purpose |
|---------|---------|
| Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore | Database error pages & migration endpoint |
| Microsoft.EntityFrameworkCore.Design | Design-time tools (already installed) |
| Microsoft.EntityFrameworkCore.Tools | Package Manager Console tools (already installed) |
## ✅ Verification
After adding this package, the following should work:
### 1. Build succeeds:
```bash
dotnet build
# Build succeeded. 0 Warning(s) 0 Error(s)
```
### 2. Database error pages work in development:
```bash
cd src/PowderCoating.Web
dotnet run
# Navigate to app without database
# You'll see helpful error page instead of crash
```
### 3. Migration endpoint works:
```bash
# In development, visit: https://localhost:7001/ef
# You'll see migration management interface
```
## 🎯 Alternative: Manual Migration
If you prefer to always run migrations manually (not use the endpoint), you can:
### Option 1: Keep the package (Recommended)
```csharp
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint(); // Helpful for development
}
```
### Option 2: Remove endpoint but keep error filter
```csharp
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
if (app.Environment.IsDevelopment())
{
// app.UseMigrationsEndPoint(); // Commented out
// You'll still get helpful error pages
}
```
### Option 3: Remove both (Not recommended)
```csharp
// builder.Services.AddDatabaseDeveloperPageExceptionFilter();
if (app.Environment.IsDevelopment())
{
// app.UseMigrationsEndPoint();
// Generic error pages only
}
```
We're using **Option 1** (recommended) for the best developer experience.
## 📋 Files Modified
1.`src/PowderCoating.Web/PowderCoating.Web.csproj` - Added diagnostics package
## 🚀 Build Status
After this fix, your build should succeed:
```bash
dotnet restore
dotnet build
# Expected Output:
# Build succeeded.
# 0 Warning(s)
# 0 Error(s)
```
## 📚 Additional Resources
- [Database Error Page Middleware](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying#apply-migrations-at-runtime)
- [Development-time Features](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling)
---
**Both errors are now fixed by adding the `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` package!** This provides helpful database error pages during development.
-155
View File
@@ -1,155 +0,0 @@
# Multi-Tenancy Implementation Status
## Completed Tasks ✅
### Phase 1: Core Infrastructure (COMPLETED)
- ✅ Created `Company` entity with all required fields
- ✅ Added `CompanyId` to `BaseEntity` (all entities now tenant-scoped)
- ✅ Updated `ApplicationUser` with `CompanyId`, `Company` navigation, and `CompanyRole`
- ✅ Created `ITenantContext` interface
- ✅ Implemented `TenantContext` service for tenant resolution
- ✅ Updated `AppConstants` with `SuperAdmin` role and `CompanyRoles` class
### Phase 2: Database Layer (COMPLETED)
- ✅ Added `Companies` DbSet to `ApplicationDbContext`
- ✅ Implemented global query filters for tenant isolation (soft delete + CompanyId filtering)
- ✅ Added foreign key relationships from all entities to Companies
- ✅ Created CompanyId indexes on all tenant-scoped entities
- ✅ Updated `SaveChangesAsync` to auto-set CompanyId on new entities
- ✅ Created EF Core migration `AddMultiTenancy`
### Phase 3: Authentication & Authorization (COMPLETED)
- ✅ Registered `ITenantContext` service in Program.cs
- ✅ Added `HttpContextAccessor` for tenant context resolution
- ✅ Configured authorization policies:
- `SuperAdminOnly` - Platform administrators
- `CompanyAdminOnly` - Company administrators
- `CanManageJobs` - Job management permissions
- `CanManageUsers` - User management permissions
- `CanViewData` - All authenticated users
### Phase 4: Data Seeding (COMPLETED)
- ✅ Updated `SeedData.cs` to create default company
- ✅ Seeds SuperAdmin user (superadmin@powdercoating.com / SuperAdmin123!)
- ✅ Seeds CompanyAdmin user (admin@demo.com / CompanyAdmin123!)
- ✅ Seeds Manager user (manager@demo.com / Manager123!)
## Completed Tasks ✅ (Continued)
### Phase 5: Company Management (COMPLETED)
- ✅ Created Company DTOs (CompanyDto, CompanyListDto, CreateCompanyDto, UpdateCompanyDto)
- ✅ Created CompaniesController for SuperAdmin with full CRUD operations
- ✅ Created Company views (Index, Create, Edit, Details)
- ✅ Created CompanyProfile for AutoMapper
- ✅ Enhanced Repository with `include` and `ignoreQueryFilters` support
### Phase 6: User Management (COMPLETED)
- ✅ Created User Management DTOs
- ✅ Created CompanyUsersController for company user management
- ✅ Created CompanyUsers views (Index, Create, Edit)
- ✅ Implemented user creation with automatic company assignment
- ✅ Implemented role-based permissions per user
### Phase 7: UI Updates (COMPLETED)
- ✅ Updated _Layout.cshtml with company badge display in header
- ✅ Added conditional navigation for SuperAdmin (Companies menu)
- ✅ Added conditional navigation for CompanyAdmin (Manage Users menu)
- ✅ Created AUTHORIZATION_UPDATE_GUIDE.md with instructions for existing controllers
### Phase 8: Ready for Deployment
- 📋 Apply database migration (see DEPLOYMENT_GUIDE.md)
- 📋 Test multi-tenancy implementation end-to-end
## Important Notes ⚠️
### Migration Status
The migration file `20260205220415_AddMultiTenancy.cs` has been created but **NOT YET APPLIED** to the database.
**IMPORTANT**: Before applying the migration, you need to handle existing data:
1. The migration adds `CompanyId` columns with `defaultValue: 0`
2. This will cause foreign key constraint violations
3. The `SeedData.cs` will create a default company and assign users to it
4. **First-time setup**: Run migration after ensuring no data exists, or manually update existing data
### Applying the Migration
```bash
cd src/PowderCoating.Web
dotnet ef database update --project ../PowderCoating.Infrastructure
```
### Default Credentials
After migration and seeding:
**Super Admin (Platform Management)**
- Email: superadmin@powdercoating.com
- Password: SuperAdmin123!
- Can: Manage all companies, view all data
**Company Admin (Demo Company)**
- Email: admin@demo.com
- Password: CompanyAdmin123!
- Can: Manage Demo Company users, manage Demo Company data
**Manager (Demo Company)**
- Email: manager@demo.com
- Password: Manager123!
- Can: Manage jobs, inventory, customers for Demo Company
## Architecture Overview
### Data Isolation
- **Global Query Filters**: All queries automatically filtered by `CompanyId`
- **SuperAdmin Bypass**: SuperAdmin can use `.IgnoreQueryFilters()` to access all data
- **Automatic CompanyId Assignment**: `SaveChangesAsync` auto-sets CompanyId on new entities
### Tenant Resolution
1. User logs in and receives `CompanyId` claim
2. `TenantContext` reads `CompanyId` from HTTP context claims
3. `ApplicationDbContext` uses `TenantContext` to apply query filters
4. All queries automatically scoped to user's company
### Role Hierarchy
- **SuperAdmin**: Platform-level (manages companies, sees all data)
- **CompanyAdmin**: Company-level (manages company users and data)
- **Manager**: Company-level (manages operations, no user management)
- **Worker**: Company-level (limited write access)
- **Viewer**: Company-level (read-only access)
## Next Steps
1. **Complete Company Management** (CompaniesController + views)
2. **Complete User Management** (CompanyUsersController + views)
3. **Update Navigation** (_Layout.cshtml)
4. **Apply Migration** (database update)
5. **End-to-End Testing**
## File Changes Summary
### New Files Created
- `src/PowderCoating.Core/Entities/Company.cs`
- `src/PowderCoating.Core/Interfaces/ITenantContext.cs`
- `src/PowderCoating.Infrastructure/Services/TenantContext.cs`
- `src/PowderCoating.Infrastructure/Migrations/20260205220415_AddMultiTenancy.cs`
### Modified Files
- `src/PowderCoating.Core/Entities/BaseEntity.cs` - Added CompanyId
- `src/PowderCoating.Core/Entities/ApplicationUser.cs` - Added CompanyId, Company, CompanyRole
- `src/PowderCoating.Infrastructure/Data/ApplicationDbContext.cs` - Query filters, relationships, auto-set CompanyId
- `src/PowderCoating.Infrastructure/Data/SeedData.cs` - Multi-tenancy seeding
- `src/PowderCoating.Shared/Constants/AppConstants.cs` - SuperAdmin role, CompanyRoles
- `src/PowderCoating.Web/Program.cs` - ITenantContext registration, authorization policies
## Known Issues / Warnings
1. **EF Warning**: "Entity 'Company' has a global query filter defined and is the required end of a relationship"
- This is expected and doesn't affect functionality
- Company navigation on ApplicationUser is nullable to handle this
2. **Migration Data Loss Warning**: "An operation was scaffolded that may result in the loss of data"
- The migration adds non-nullable CompanyId columns
- Existing data will have CompanyId=0 initially
- SeedData creates default company and should assign users to it
- Manual data migration may be needed for existing production data
View File
-134
View File
@@ -1,134 +0,0 @@
# .NET 10.0 Version Notice
## Important Information
This project has been configured to target **.NET 10.0** as requested. However, please note:
### Current Status (as of February 2026)
⚠️ **.NET 10.0 is NOT yet released** - The current latest stable version is .NET 8.0 LTS.
Microsoft's .NET release schedule:
- ✅ .NET 8.0 LTS - Released November 2023 (Current LTS, supported until November 2026)
- 🚧 .NET 9.0 - Released November 2024 (STS - Standard Term Support)
- ❓ .NET 10.0 - Expected November 2025 (if following Microsoft's pattern)
### What This Means
1. **Package Versions**: The NuGet package versions specified (e.g., Version="10.0.0") may not exist yet
2. **SDK Requirement**: You'll need the .NET 10.0 SDK when it becomes available
3. **Current Alternative**: You can easily convert this project back to .NET 8.0 if needed
### Converting Back to .NET 8.0 (If Needed)
If .NET 10.0 is not yet available and you want to use this project now:
1. **Find and Replace in all .csproj files:**
- Change `<TargetFramework>net10.0</TargetFramework>` to `<TargetFramework>net8.0</TargetFramework>`
2. **Update Package Versions:**
- Microsoft.AspNetCore.* packages: `10.0.0``8.0.0`
- Microsoft.EntityFrameworkCore.* packages: `10.0.0``8.0.0`
- Microsoft.Extensions.* packages: `10.0.0``8.0.0`
- FluentValidation: `11.10.0``11.9.0`
- Semantic Kernel: `1.31.0``1.0.1`
- ML.NET: `4.0.0``3.0.1`
- Serilog.AspNetCore: `8.0.3``8.0.0`
- Serilog.Sinks.File: `6.0.0``5.0.0`
- Swashbuckle: `7.2.0``6.5.0`
- Microsoft.NET.Test.Sdk: `17.12.0``17.8.0`
- xunit: `2.9.2``2.6.2`
- xunit.runner.visualstudio: `2.8.2``2.5.4`
- Moq: `4.20.72``4.20.70`
- coverlet.collector: `6.0.2``6.0.0`
3. **Run:**
```bash
dotnet restore
dotnet build
```
### Quick Conversion Script
You can use this PowerShell script to convert all projects to .NET 8.0:
```powershell
# Navigate to solution root
cd PowderCoatingApp
# Replace net10.0 with net8.0 in all .csproj files
Get-ChildItem -Recurse -Filter *.csproj | ForEach-Object {
(Get-Content $_.FullName) -replace 'net10.0', 'net8.0' | Set-Content $_.FullName
(Get-Content $_.FullName) -replace 'Version="10.0.0"', 'Version="8.0.0"' | Set-Content $_.FullName
}
# Restore packages
dotnet restore
```
Or use this bash script (Linux/Mac):
```bash
# Navigate to solution root
cd PowderCoatingApp
# Replace net10.0 with net8.0 in all .csproj files
find . -name "*.csproj" -type f -exec sed -i 's/net10.0/net8.0/g' {} +
find . -name "*.csproj" -type f -exec sed -i 's/Version="10.0.0"/Version="8.0.0"/g' {} +
# Restore packages
dotnet restore
```
### When .NET 10.0 Becomes Available
Once .NET 10.0 is officially released:
1. **Install the SDK:**
```bash
dotnet --list-sdks
# Should show 10.0.xxx
```
2. **Restore packages:**
```bash
dotnet restore
```
3. **Verify all packages are available:**
```bash
dotnet build
```
4. **Update to latest patch versions** as they become available
### Project Structure Compatibility
The project structure, architecture, and code are designed to be forward-compatible:
- ✅ Clean Architecture principles work across all .NET versions
- ✅ Entity Framework Core patterns remain consistent
- ✅ ASP.NET Core MVC structure is stable
- ✅ Identity system is backwards compatible
- ✅ Repository pattern implementation is framework-agnostic
### Recommendations
For **production use today**:
- Use .NET 8.0 LTS (Long Term Support until November 2026)
- Convert the project using the scripts above
- All functionality will work identically
For **future-proofing**:
- Keep the .NET 10.0 configuration
- Wait for the official release
- Test thoroughly when upgrading
### Questions?
If you need help converting to .NET 8.0 or have questions about .NET versions, please refer to:
- [.NET Support Policy](https://dotnet.microsoft.com/platform/support/policy)
- [.NET Release Schedule](https://github.com/dotnet/core/blob/main/releases.md)
---
**Note**: This project structure is production-ready and will work with any .NET version 8.0 or higher with minimal modifications to the target framework and package versions.
-546
View File
@@ -1,546 +0,0 @@
# Next Steps - Your Powder Coating Application is Ready!
## 🎉 Current Status: BUILD SUCCESSFUL! ✅
Congratulations! Your application builds without errors. Here's your roadmap to get it running and start building features.
---
## 🚀 IMMEDIATE ACTION: Get the App Running (Do This First!)
### Step 1: Create the Database (5 minutes)
```bash
cd src/PowderCoating.Web
# Create the initial migration
dotnet ef migrations add InitialCreate --project ../PowderCoating.Infrastructure
# Apply it to create the database
dotnet ef database update --project ../PowderCoating.Infrastructure
```
**✅ What This Does:**
- Creates `PowderCoatingDb` database in SQL Express
- Creates 25+ tables (Customers, Jobs, Quotes, Inventory, etc.)
- Seeds 3 pricing tiers (Standard, Preferred, Premium)
- Creates 5 roles (Administrator, Manager, Employee, ShopFloor, ReadOnly)
- Creates admin user: `admin@powdercoating.com` / `Admin123!`
### Step 2: Run the Application (1 minute)
```bash
# Still in src/PowderCoating.Web
dotnet run
```
**You should see:**
```
info: Now listening on: https://localhost:7001
```
### Step 3: Login and Verify (2 minutes)
1. Open browser: **https://localhost:7001**
2. Click **Login** (top right)
3. Use credentials:
- Email: `admin@powdercoating.com`
- Password: `Admin123!`
**✅ Success!** You should be logged in as administrator!
---
## 📋 Next: Build Your First Feature (Customer Management)
Now let's add actual functionality. Start with **Customer Management** because every job needs a customer.
### Create the Customers Controller
Create file: `src/PowderCoating.Web/Controllers/CustomersController.cs`
```csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using AutoMapper;
using PowderCoating.Core.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Application.DTOs.Customer;
namespace PowderCoating.Web.Controllers;
[Authorize]
public class CustomersController : Controller
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly ILogger<CustomersController> _logger;
public CustomersController(
IUnitOfWork unitOfWork,
IMapper mapper,
ILogger<CustomersController> logger)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_logger = logger;
}
// GET: Customers
public async Task<IActionResult> Index()
{
var customers = await _unitOfWork.Customers.GetAllAsync();
var customerDtos = _mapper.Map<List<CustomerListDto>>(customers);
return View(customerDtos);
}
// GET: Customers/Details/5
public async Task<IActionResult> Details(int id)
{
var customer = await _unitOfWork.Customers.GetByIdAsync(id);
if (customer == null)
{
return NotFound();
}
var customerDto = _mapper.Map<CustomerDto>(customer);
return View(customerDto);
}
// GET: Customers/Create
public IActionResult Create()
{
return View(new CreateCustomerDto());
}
// POST: Customers/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreateCustomerDto dto)
{
if (!ModelState.IsValid)
{
return View(dto);
}
try
{
var customer = _mapper.Map<Customer>(dto);
await _unitOfWork.Customers.AddAsync(customer);
await _unitOfWork.SaveChangesAsync();
TempData["Success"] = "Customer created successfully!";
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating customer");
ModelState.AddModelError("", "An error occurred while creating the customer.");
return View(dto);
}
}
// GET: Customers/Edit/5
public async Task<IActionResult> Edit(int id)
{
var customer = await _unitOfWork.Customers.GetByIdAsync(id);
if (customer == null)
{
return NotFound();
}
var dto = _mapper.Map<UpdateCustomerDto>(customer);
return View(dto);
}
// POST: Customers/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, UpdateCustomerDto dto)
{
if (id != dto.Id)
{
return NotFound();
}
if (!ModelState.IsValid)
{
return View(dto);
}
try
{
var customer = await _unitOfWork.Customers.GetByIdAsync(id);
if (customer == null)
{
return NotFound();
}
_mapper.Map(dto, customer);
await _unitOfWork.Customers.UpdateAsync(customer);
await _unitOfWork.SaveChangesAsync();
TempData["Success"] = "Customer updated successfully!";
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating customer {CustomerId}", id);
ModelState.AddModelError("", "An error occurred while updating the customer.");
return View(dto);
}
}
// GET: Customers/Delete/5
public async Task<IActionResult> Delete(int id)
{
var customer = await _unitOfWork.Customers.GetByIdAsync(id);
if (customer == null)
{
return NotFound();
}
var dto = _mapper.Map<CustomerDto>(customer);
return View(dto);
}
// POST: Customers/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
try
{
await _unitOfWork.Customers.SoftDeleteAsync(id);
await _unitOfWork.SaveChangesAsync();
TempData["Success"] = "Customer deleted successfully!";
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting customer {CustomerId}", id);
TempData["Error"] = "An error occurred while deleting the customer.";
return RedirectToAction(nameof(Index));
}
}
}
```
### Add Missing AutoMapper Mappings
The `UpdateCustomerDto` mappings are missing. Update `CustomerProfile.cs`:
```csharp
public class CustomerProfile : Profile
{
public CustomerProfile()
{
CreateMap<Customer, CustomerDto>()
.ForMember(dest => dest.PricingTierName,
opt => opt.MapFrom(src => src.PricingTier != null ? src.PricingTier.TierName : null));
CreateMap<CreateCustomerDto, Customer>();
CreateMap<UpdateCustomerDto, Customer>(); // ← ADD THIS
CreateMap<Customer, UpdateCustomerDto>(); // ← ADD THIS TOO
CreateMap<Customer, CustomerListDto>()
.ForMember(dest => dest.ContactName,
opt => opt.MapFrom(src => !string.IsNullOrEmpty(src.ContactFirstName) || !string.IsNullOrEmpty(src.ContactLastName)
? $"{src.ContactFirstName} {src.ContactLastName}".Trim()
: string.Empty));
}
}
```
### Create a Basic Index View
Create file: `src/PowderCoating.Web/Views/Customers/Index.cshtml`
```html
@model List<PowderCoating.Application.DTOs.Customer.CustomerListDto>
@{
ViewData["Title"] = "Customers";
}
<div class="container-fluid">
<div class="row mb-3">
<div class="col">
<h2>Customers</h2>
</div>
<div class="col text-end">
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Customer
</a>
</div>
</div>
@if (TempData["Success"] != null)
{
<div class="alert alert-success alert-dismissible fade show">
@TempData["Success"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<div class="card">
<div class="card-body">
@if (!Model.Any())
{
<p class="text-muted">No customers found. Click "Add New Customer" to get started.</p>
}
else
{
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Company Name</th>
<th>Contact</th>
<th>Email</th>
<th>Phone</th>
<th>Type</th>
<th>Balance</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var customer in Model)
{
<tr>
<td>@customer.CompanyName</td>
<td>@customer.ContactName</td>
<td>@customer.Email</td>
<td>@customer.Phone</td>
<td>
@if (customer.IsCommercial)
{
<span class="badge bg-primary">Commercial</span>
}
else
{
<span class="badge bg-secondary">Non-Commercial</span>
}
</td>
<td>@customer.CurrentBalance.ToString("C")</td>
<td>
@if (customer.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</td>
<td>
<a asp-action="Details" asp-route-id="@customer.Id" class="btn btn-sm btn-info">Details</a>
<a asp-action="Edit" asp-route-id="@customer.Id" class="btn btn-sm btn-warning">Edit</a>
<a asp-action="Delete" asp-route-id="@customer.Id" class="btn btn-sm btn-danger">Delete</a>
</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
</div>
```
### Add to Navigation Menu
Edit `src/PowderCoating.Web/Views/Shared/_Layout.cshtml` and add the Customers link:
```html
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-controller="Customers" asp-action="Index">Customers</a>
</li>
</ul>
```
### Test It!
1. Run the app: `dotnet run`
2. Navigate to: https://localhost:7001/Customers
3. You should see an empty customer list with "Add New Customer" button
---
## 📋 Recommended Development Order
Build features in this sequence for maximum value:
### Week 1: Core Data Management
1.**Customers** (you just started this!)
- Complete Create/Edit forms
- Add validation
- Test CRUD operations
2. **Inventory Items**
- Powder colors and materials
- SKU tracking
- Reorder alerts
3. **Suppliers**
- Manage powder suppliers
- Contact information
### Week 2: Job Quoting
4. **Quotes**
- Create quotes for customers
- Line items with pricing
- Quote templates
5. **Quote to Job Conversion**
- Approve quote → Create job
- Transfer all details
### Week 3: Job Management
6. **Jobs**
- Job creation
- Status workflow (15 stages)
- Job items
7. **Job Assignment**
- Assign to employees
- Track progress
8. **Job Photos & Notes**
- Upload before/after photos
- Internal and customer notes
### Week 4: Shop Floor
9. **Shop Floor Display**
- Real-time job board
- Color-coded by priority
- TV-optimized view
10. **SignalR Integration**
- Real-time status updates
- Auto-refresh displays
### Week 5+: Advanced Features
11. **Equipment Management**
12. **Maintenance Tracking**
13. **Reporting & Analytics**
14. **AI-Powered Quoting**
15. **Customer Portal**
---
## 🛠️ Essential Commands Reference
### Database Commands:
```bash
# Add migration after changing entities
dotnet ef migrations add MigrationName --project src/PowderCoating.Infrastructure --startup-project src/PowderCoating.Web
# Update database
dotnet ef database update --project src/PowderCoating.Infrastructure --startup-project src/PowderCoating.Web
# Rollback one migration
dotnet ef database update PreviousMigration --project src/PowderCoating.Infrastructure --startup-project src/PowderCoating.Web
# List all migrations
dotnet ef migrations list --project src/PowderCoating.Infrastructure --startup-project src/PowderCoating.Web
```
### Development:
```bash
# Run with auto-reload
dotnet watch run
# Build
dotnet build
# Clean
dotnet clean
# Run tests
dotnet test
```
---
## 💡 Development Tips
### 1. Use `dotnet watch run`
Auto-reloads when you save files - huge time saver!
### 2. Check the Logs
Serilog writes to:
- Console (see terminal)
- File: `logs/powdercoating-YYYYMMDD.txt`
### 3. Bootstrap is Ready
Use Bootstrap 5 classes:
- Tables: `.table`, `.table-striped`, `.table-hover`
- Buttons: `.btn`, `.btn-primary`, `.btn-sm`
- Cards: `.card`, `.card-body`
- Forms: `.form-control`, `.form-label`
### 4. Test Incrementally
Test each feature immediately after building it. Don't wait!
### 5. Commit Often
```bash
git add .
git commit -m "Add customer management"
```
---
## ✅ Your Immediate To-Do List
- [ ] Create database (Step 1 above)
- [ ] Run the app and login (Steps 2-3)
- [ ] Create CustomersController
- [ ] Add missing AutoMapper mappings
- [ ] Create Index view
- [ ] Add navigation menu item
- [ ] Test viewing empty customer list
- [ ] Create the Create.cshtml form
- [ ] Test adding your first customer
- [ ] Create Edit and Details views
- [ ] Test full CRUD operations
---
## 🎯 Success Criteria
You'll know you're on track when:
✅ Database created successfully
✅ Can login as admin
✅ Can navigate to /Customers
✅ See empty list with "Add New Customer" button
✅ Can create a customer
✅ Customer appears in the list
✅ Can edit the customer
✅ Can view customer details
✅ Can delete (soft delete) the customer
---
## 🚀 You're Ready to Build!
You have:
- ✅ Solid architecture (Clean Architecture pattern)
- ✅ Database ready (Entity Framework Core)
- ✅ Authentication working (ASP.NET Identity)
- ✅ AutoMapper configured
- ✅ Both Web and API projects
- ✅ Repository pattern implemented
- ✅ Logging set up (Serilog)
**Start with the Customer Management module and you'll be up and running in no time!**
Need help with specific features? Just ask! 🎉
-157
View File
@@ -1,157 +0,0 @@
# Package Downgrade Error - FIXED
## 🐛 Error Found
```
Warning As Error: Detected package downgrade: Microsoft.Extensions.Logging.Abstractions from 10.0.0 to 8.0.2
Reference the package directly from the project to select a different version.
PowderCoating.Application -> AutoMapper 16.0.0 -> Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
PowderCoating.Application -> Microsoft.Extensions.Logging.Abstractions (>= 8.0.2)
```
## 🔍 Root Cause
**AutoMapper 16.0.0** requires `Microsoft.Extensions.Logging.Abstractions >= 10.0.0`
However, the Application project had it pinned to version `8.0.2`, which is incompatible.
## ✅ Fix Applied
Updated `PowderCoating.Application.csproj`:
**Before:**
```xml
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
```
**After:**
```xml
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
```
## 📦 Package Version Compatibility Matrix
### AutoMapper 16.0.0 Requirements
AutoMapper 16.0.0 requires the following minimum versions:
| Package | Minimum Version | Our Version | Status |
|---------|----------------|-------------|--------|
| Microsoft.Extensions.Logging.Abstractions | 10.0.0 | **10.0.0** | ✅ |
| .NET | 8.0 | 8.0 | ✅ |
### Why Version 10.0.0?
`Microsoft.Extensions.Logging.Abstractions 10.0.0` is part of the **.NET 9.0** package family, but it's **fully compatible** with .NET 8.0 projects.
Microsoft's versioning strategy:
- .NET 8.0 packages → Version 8.x.x
- .NET 9.0 packages → Version 9.x.x
- .NET 10.0 packages → Version 10.x.x
Even though we're on .NET 8.0, we can (and should) use the 10.0.0 version of this package because:
1. AutoMapper 16.0.0 requires it
2. It's backward compatible with .NET 8.0
3. Microsoft supports this scenario
## 🔄 Impact on Other Projects
This change affects the Application project only. Other projects indirectly benefit:
### Infrastructure Project
- References Application project
- Will get Microsoft.Extensions.Logging.Abstractions 10.0.0 transitively
- ✅ No changes needed
### Web Project
- References Application and Infrastructure
- Will get correct version transitively
- ✅ No changes needed
### API Project
- References Application and Infrastructure
- Will get correct version transitively
- ✅ No changes needed
## 📋 Updated Package Versions
### PowderCoating.Application
```xml
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.0.0" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" /> ← UPDATED
<PackageReference Include="Microsoft.SemanticKernel" Version="1.31.0" />
<PackageReference Include="Microsoft.ML" Version="3.0.1" />
</ItemGroup>
```
## ✅ Verification
After this fix, the build should succeed:
```bash
# Clean the solution
dotnet clean
# Restore packages
dotnet restore
# Build
dotnet build
# Expected Output:
# Build succeeded.
# 0 Warning(s)
# 0 Error(s)
```
## 🎯 Why This Happened
When I initially updated packages, I tried to keep everything on .NET 8.0 versions (8.x.x). However:
1. AutoMapper 16.0.0 was released **after** .NET 8.0
2. It uses newer package versions from the .NET 9.0/10.0 era
3. The package `Microsoft.Extensions.Logging.Abstractions 10.0.0` is required
This is normal and expected when using the latest packages!
## 📝 Package Version Strategy
Going forward, here's the versioning approach:
### Core .NET Packages (Must Match Target Framework)
- TargetFramework: `net8.0`
- Microsoft.AspNetCore.* → 8.0.11 ✅
- Microsoft.EntityFrameworkCore.* → 8.0.11 ✅
### Extension Packages (Can Be Newer)
- Microsoft.Extensions.Logging.Abstractions → **10.0.0** ✅ (required by AutoMapper)
- AutoMapper → 16.0.0 ✅
- FluentValidation → 11.11.0 ✅
- Serilog → 8.0.3 ✅
This is a **supported configuration** by Microsoft!
## 🔬 Testing
The updated package will not affect functionality. The logging abstractions are interfaces, and version 10.0.0 is fully compatible with .NET 8.0 runtimes.
**No code changes required** - just the package version update.
## 🚀 Ready to Build
Your project should now build without the downgrade warning:
```bash
dotnet restore
dotnet build
```
Expected result: ✅ **Build succeeded. 0 Warning(s) 0 Error(s)**
---
**Package downgrade error resolved!** The project now has the correct version of `Microsoft.Extensions.Logging.Abstractions` to satisfy AutoMapper 16.0.0's requirements.
-3
View File
@@ -1,3 +0,0 @@
Batch Pricing Formula
=======================
Price = (Material + Labor + Overhead + Additional) × (1 + Reject%) ÷ (1 Margin%) × Complexity Factor
-206
View File
@@ -1,206 +0,0 @@
# Quick CSV Import Test Guide
## Starting the Application
```bash
cd src/PowderCoating.Web
dotnet run
```
Access at: https://localhost:58461
## Login Credentials
- **SuperAdmin**: `superadmin@powdercoating.com` / `SuperAdmin123!`
- **Company Admin**: `admin@demo.com` / `CompanyAdmin123!`
## Test Steps
### 1. Navigate to CSV Import
1. Login to the application
2. Click **Tools** in the navigation menu
3. Scroll down to the **CSV Bulk Import** card (yellow border)
4. You'll see 3 tabs: Customers, Catalog Items, Inventory
### 2. Test Customer Import
#### Download Template
1. Click on **Customers** tab
2. Click **Download Customer Template** button
3. Open the downloaded CSV file in Excel or text editor
4. You'll see headers and one example row
#### Modify Template
Add a few test customers:
```csv
CompanyName,ContactName,Email,Phone,Address,City,State,ZipCode,CustomerType,PricingTierCode,CreditLimit,PaymentTerms,TaxExempt,Notes
Test Company 1,Alice Smith,alice@test1.com,555-0001,100 Test St,Chicago,IL,60601,Commercial,Gold,10000,Net 30,false,Test customer 1
Test Company 2,Bob Jones,bob@test2.com,555-0002,200 Test Ave,Chicago,IL,60602,Commercial,Silver,5000,Net 15,false,Test customer 2
Home User,John Doe,john@home.com,555-0003,300 Home Ln,Chicago,IL,60603,Individual,,0,Cash,true,Individual customer
```
#### Import
1. Save the CSV file
2. Click **Choose File** in the Upload section
3. Select your modified CSV file
4. Click **Import Customers** button
5. Watch for:
- Loading spinner appears
- After processing, results card appears showing:
- Number of records imported (green)
- Number of errors (red)
- Total rows processed (blue)
- Toast notification in bottom-right corner
6. Verify by going to **Customers** page - you should see your new customers
### 3. Test Catalog Item Import
#### Download Template
1. Click on **Catalog Items** tab
2. Click **Download Catalog Template** button
3. Open the downloaded CSV file
#### Modify Template
Add items with hierarchical categories:
```csv
CategoryPath,ItemName,SKU,Description,BasePrice,UnitOfMeasure,EstimatedWeight,EstimatedSurfaceArea,RequiresSandblasting,RequiresMasking,IsActive
Automotive/Wheels/Standard,Car Wheel 17",WHL-17-STD,Standard 17 inch wheel,85.00,each,18.0,5.0,true,true,true
Automotive/Wheels/Performance,Car Wheel 18" Sport,WHL-18-SPORT,Performance 18 inch wheel,120.00,each,20.0,5.5,true,true,true
Industrial/Railings/Commercial,Stair Handrail 6ft,RAIL-6FT-STR,6 foot stair handrail,95.00,section,15.0,8.0,true,false,true
Furniture/Outdoor/Patio,Patio Chair Frame,FURN-CHAIR-PAT,Patio chair metal frame,45.00,each,12.0,6.0,true,false,true
```
#### Import
1. Save and import the CSV file
2. Check results - you should see 4 items imported
3. **Important**: The categories will be auto-created!
- "Automotive" → "Wheels" → "Standard"
- "Automotive" → "Wheels" → "Performance"
- "Industrial" → "Railings" → "Commercial"
- "Furniture" → "Outdoor" → "Patio"
4. Verify by going to **Catalog** page - browse the category tree
### 4. Test Inventory Item Import
#### Download Template
1. Click on **Inventory** tab
2. Click **Download Inventory Template** button
#### Modify Template
Add powder coating inventory:
```csv
SKU,ItemName,CategoryName,Manufacturer,ColorName,ColorCode,QuantityInStock,UnitOfMeasure,UnitCost,ReorderPoint,ReorderQuantity,Notes
PWD-RED-001,Red Powder Coating,Powder Coatings,Tiger Drylac,Red,RAL 3020,400,lbs,3.85,80,150,Traffic red
PWD-BLU-001,Blue Powder Coating,Powder Coatings,Tiger Drylac,Blue,RAL 5005,300,lbs,3.95,60,120,Signal blue
PWD-GRN-001,Green Powder Coating,Powder Coatings,Axalta,Green,RAL 6018,250,lbs,4.10,50,100,Yellow green
SAND-MEDIA-001,Sandblasting Media,Consumables,Generic,Brown,,500,lbs,1.25,100,300,Aluminum oxide
```
#### Import
1. Save and import the CSV file
2. Check results - should show 4 items imported
3. Verify by going to **Inventory** page
### 5. Test Error Handling
#### Duplicate Detection
1. Try importing the same CSV file again
2. You should see:
- 0 records imported
- Warnings about duplicate emails/SKUs
- Detailed list of skipped rows
#### Missing Required Fields
Create a CSV with missing CompanyName:
```csv
CompanyName,ContactName,Email,Phone,Address,City,State,ZipCode,CustomerType,PricingTierCode,CreditLimit,PaymentTerms,TaxExempt,Notes
,Missing Company,missing@test.com,555-9999,,,,,,,,,
```
Expected: Error message "Row 2: CompanyName is required."
#### Invalid Pricing Tier
```csv
CompanyName,ContactName,Email,Phone,Address,City,State,ZipCode,CustomerType,PricingTierCode,CreditLimit,PaymentTerms,TaxExempt,Notes
Test Company,Jane Doe,jane@test.com,555-8888,400 Test St,Chicago,IL,60604,Commercial,Diamond,15000,Net 30,false,Invalid tier
```
Expected: Warning "Pricing tier 'Diamond' not found. Customer will have no pricing tier." but customer still imported.
### 6. Verify Results
After all imports:
1. **Customers Page**:
- Should see all imported customers
- Check that pricing tiers are assigned correctly
- Verify contact information is accurate
2. **Catalog Page**:
- Expand category tree to see hierarchical structure
- Verify all items are under correct categories
- Check that prices and SKUs are correct
3. **Inventory Page**:
- See all powder coatings and consumables
- Verify quantities, costs, and reorder points
- Check color codes and manufacturer info
## Common Issues
### Issue: "No file provided or file is empty"
**Solution**: Make sure you selected a file before clicking Import
### Issue: "Only CSV files are allowed"
**Solution**: Save file as .csv (not .xlsx or .txt)
### Issue: "Pricing tier 'XXX' not found"
**Solution**: Use Standard, Silver, Gold, or Platinum (case-insensitive)
### Issue: Categories not showing up
**Solution**:
- Check CategoryPath format: "Parent/Child/GrandChild"
- No leading/trailing slashes
- Use forward slashes only
### Issue: Build errors
**Solution**: Make sure CsvHelper is installed:
```bash
cd src/PowderCoating.Infrastructure
dotnet add package CsvHelper
cd ../PowderCoating.Application
dotnet add package CsvHelper
```
## Success Indicators
✅ Templates download successfully
✅ CSV files import without errors
✅ Success counts match expected numbers
✅ Data appears in respective pages (Customers, Catalog, Inventory)
✅ Categories auto-created for catalog items
✅ Duplicates are detected and skipped
✅ Error messages are clear and actionable
✅ Toast notifications appear
✅ Results card shows detailed summary
## Next Steps
Once basic import is working:
1. Try importing larger files (50+ rows)
2. Test with deep category hierarchies (4-5 levels)
3. Verify multi-tenancy (different companies can't see each other's imports)
4. Export QuickBooks data and re-import via CSV
5. Use CSV import for initial data seeding instead of manual entry
## Performance Benchmarks
Expected import times (approximate):
- 10 rows: < 1 second
- 50 rows: < 3 seconds
- 100 rows: < 5 seconds
- 500 rows: < 15 seconds
- 1000 rows: < 30 seconds
Times will vary based on:
- Number of categories to create
- Database size
- Server performance
-142
View File
@@ -1,142 +0,0 @@
# Quick Testing Guide - Lookup Management Feature
## Prerequisites
1. Database migration applied: `ConvertEnumsToLookupTables`
2. Application built successfully
3. Seed data loaded (via Platform Management > Seed Data)
## 5-Minute Quick Test
### Test 1: View Existing Lookups (2 minutes)
1. Start the application: `cd src/PowderCoating.Web && dotnet run`
2. Navigate to: `https://localhost:58461`
3. Login as SuperAdmin: `superadmin@powdercoating.com` / `SuperAdmin123!`
4. Click: **Company Settings** (in left sidebar)
5. Click: **Data Lookups** tab
6. **Expected**: See 3 sub-tabs: Job Statuses, Job Priorities, Quote Statuses
7. **Verify**: Job Statuses shows 16 default statuses with color badges
8. **Verify**: Usage counts display (e.g., "5 jobs")
### Test 2: Create Custom Lookup (1 minute)
1. On Job Statuses sub-tab, click: **Add Job Status**
2. Enter code: `TEST_STATUS`
3. Enter display name: `Test Status`
4. **Expected**: Success toast notification
5. **Verify**: New status appears in table at bottom
### Test 3: Edit Custom Lookup (1 minute)
1. Find the "Test Status" row
2. Click: **Edit** button (pencil icon)
3. Change display name to: `My Custom Status`
4. **Expected**: Success toast notification
5. **Verify**: Table refreshes with new name
### Test 4: Delete Custom Lookup (1 minute)
1. Find the "My Custom Status" row
2. Click: **Delete** button (trash icon)
3. Confirm deletion
4. **Expected**: Success toast notification
5. **Verify**: Status removed from table
### Test 5: Verify Jobs Use Lookups (30 seconds)
1. Navigate to: **Jobs** > **Create New Job**
2. **Verify**: Status dropdown shows all statuses (including any custom ones)
3. **Verify**: Priority dropdown shows all priorities
4. Create a job and **verify** it saves successfully
## System Protection Tests
### Test 6: Try to Delete System-Defined Status
1. Go to: Company Settings > Data Lookups > Job Statuses
2. Find "Pending" status (marked with [System] badge)
3. **Verify**: Delete button is DISABLED with tooltip "System-defined"
### Test 7: Try to Delete In-Use Status
1. Create a job with status "In Preparation"
2. Go to: Company Settings > Data Lookups > Job Statuses
3. Try to delete "In Preparation" status
4. **Expected**: Error message "Status is in use and cannot be deleted"
## Visual Verification
### Color Badges
- ✅ Statuses display with colored badges (primary, success, warning, danger, etc.)
- ✅ Badge colors match across Jobs Index and Company Settings
### Usage Counts
- ✅ Each lookup shows accurate count (e.g., "12 jobs")
- ✅ Counts update after creating/deleting jobs
### Responsive Design
- ✅ Tables display correctly on desktop
- ✅ Tables scroll horizontally on mobile if needed
## Troubleshooting
### Issue: "No job statuses found"
**Solution**: Run seed data via Platform Management > Seed Data
### Issue: Dropdowns empty in Jobs/Quotes
**Solution**: Check browser console for errors; verify migration applied
### Issue: Delete button not working
**Solution**: Check browser console; verify status is not in use
### Issue: Changes not saving
**Solution**: Check browser console for AJAX errors; verify anti-forgery token
## Success Indicators
- ✅ All 16 default job statuses visible
- ✅ All 5 default priorities visible
- ✅ All 7 default quote statuses visible
- ✅ Can add custom lookups
- ✅ Can edit custom lookups (display name only)
- ✅ Can delete unused custom lookups
- ✅ System-defined lookups protected
- ✅ In-use lookups protected
- ✅ Usage counts accurate
- ✅ Jobs/Quotes can be created with new lookups
- ✅ Color badges display correctly
## Next Steps After Testing
If all tests pass:
- ✅ Mark feature as production-ready
- ✅ Document for users in help guide
- ✅ Train users on lookup customization
If issues found:
- 📝 Document the issue with screenshots
- 🐛 Report to development team
- 🔄 Apply fixes and re-test
## Advanced Testing (Optional)
### Multi-Tenancy Test
1. Login as Company Admin: `admin@demo.com` / `CompanyAdmin123!`
2. Go to: Company Settings > Data Lookups
3. **Verify**: Only see lookups for YOUR company (not other companies)
4. Create a custom status for Demo Company
5. Login as different company admin
6. **Verify**: Custom status NOT visible to other company
### Performance Test
1. Create 1,000 test jobs via seed data
2. Navigate to: Jobs > Index
3. **Verify**: Page loads in < 2 seconds
4. Filter by status
5. **Verify**: Filtering is instant
### Quote Conversion Test
1. Create a quote with status "Draft"
2. Try to convert to job
3. **Expected**: Error "Only approved quotes can be converted"
4. Change quote status to "Approved"
5. Convert to job
6. **Verify**: Conversion succeeds
7. **Verify**: Quote status changes to "Converted"
---
**Testing Duration**: 5-15 minutes
**Last Updated**: February 13, 2026
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

-101
View File
@@ -1,101 +0,0 @@
# Razor @ Symbol Fix
## 🐛 Issue Found
**Error:** "The name 'media' does not exist in the current context"
**Location:** Views with CSS `@media` queries
## 🔍 Root Cause
In Razor views (.cshtml files), the `@` symbol has special meaning - it's used to switch from HTML to C# code. When you write CSS directly in a Razor view, you need to escape `@` symbols in CSS at-rules like `@media`.
## ✅ Fix Applied
Changed all `@media` to `@@media` in:
1.`Views/Home/Index.cshtml` (line 217)
2.`Views/Shared/_Layout.cshtml` (line 233)
## 📝 The Fix
### Before (❌ Causes Error):
```css
<style>
@media (max-width: 768px) {
/* styles */
}
</style>
```
### After (✅ Correct):
```css
<style>
@@media (max-width: 768px) {
/* styles */
}
</style>
```
## 💡 Why Double @@?
In Razor syntax:
- `@` = Switch to C# code
- `@@` = Escape sequence that outputs a single `@` character
So `@@media` in Razor becomes `@media` in the final HTML.
## 🎯 Other CSS At-Rules That Need Escaping
If you add any of these CSS at-rules in Razor views, remember to escape them:
```css
@@media (min-width: 1024px) { } /* Media queries */
@@keyframes fadeIn { } /* Animations */
@@import url('fonts.css'); /* Imports */
@@font-face { } /* Custom fonts */
@@supports (display: grid) { } /* Feature queries */
```
## ✅ Best Practice: External CSS
To avoid this issue entirely, you can:
### Option 1: Use External CSS File (Recommended)
Create `wwwroot/css/site.css` and link it in your layout:
```html
<link rel="stylesheet" href="~/css/site.css" />
```
No need to escape @ symbols in .css files!
### Option 2: Keep Inline (Current Approach)
Just remember to use `@@` for all CSS at-rules.
## 🔧 If You See This Error Again
1. **Look for CSS at-rules** in your .cshtml file
2. **Find any `@` symbols** in `<style>` tags
3. **Double them** → Change `@` to `@@`
4. **Rebuild** and the error will be gone
## 🎯 Quick Test
The error is now fixed! When you run the app:
```bash
cd src/PowderCoating.Web
dotnet run
```
Navigate to `https://localhost:7001` and you should see the beautiful login page without errors!
## 📋 Files Fixed
1.`Views/Home/Index.cshtml` - Login page
2.`Views/Shared/_Layout.cshtml` - Main layout
Both now properly escape the @ symbol in CSS media queries.
---
**The Razor parsing error is now fixed! The login page should render perfectly.**
-59
View File
@@ -1,59 +0,0 @@
# Release Notes — 2026-04-06
## QuickBooks Desktop Migration Wizard — Import Quality & UX
### Bug Fixes
**Customer Payments Import (607 skipped → 614 imported)**
- Invoice import (Step 6) no longer creates Payment records. AmountPaid and Status are set correctly from the balance detail file, but Payment record creation is deferred to Step 7 so that customer payment history includes richer data (check numbers, bank account names, exact payment dates).
- Step 7 now correctly creates payment records for pre-settled invoices without duplicating them.
**Vendor Bills Import — App Crash**
- Eliminated 1,000+ mid-loop database round trips during the bills import. Bills and their line items are now saved in a single batch, preventing request timeouts on large imports.
**QuickBooks Online — QB Desktop Parity**
- Verified QB Online migration does not share the payment matching bugs fixed on the Desktop side.
---
### UX Improvements
**Import Result Reporting**
- "Skipped" badge changed from yellow (warning) to gray — skips are not failures.
- Added "Already Recorded" badge (gray) for records that were intentionally not re-imported because they already exist. No longer shown as "Skipped."
- Vendor Bills & Payments step now shows **"X Bills Imported / Y Payments Applied"** as the primary result instead of a confusing combined Total/Imported count where Imported > Total.
**False-Alarm Warnings Eliminated**
The following QB structural row types and report artifacts now silently pass through all importers without generating warnings:
| Step | Previously Warned | Now |
|---|---|---|
| Chart of Accounts | NONPOSTING accounts (Estimates, Purchase Orders) | Silent |
| Catalog Items | Non-service types (DISC, GRP, PMT, OTHC), group markers | Silent |
| Inventory | Category/group headers, Total/TOTAL subtotal rows | Silent |
| Vendor Bills | Bill Pmt -Check, Item Receipt, Credits, and all other non-Bill rows | Silent |
| Vendor Payments | Item Receipt, Credits, zero-amount payment rows, and all other non-Bill Pmt rows | Silent |
**General Rule Applied**
Each importer now silently ignores any row type it doesn't own, rather than maintaining a whitelist. This prevents unexpected warnings when customers have QB files containing row types not seen during testing (Credits, Journal Entries, Purchase Orders, etc.).
---
### Seed Data
- `SeedDataService` is now re-runnable — each seeder runs in its own try/catch with `ChangeTracker.Clear()` on failure, so one failing seeder no longer aborts the rest.
- Added `SeedBillsAsync` (4 bills: Paid, PartiallyPaid, 2× Open) and `SeedExpensesAsync` (5 expenses) for demo company.
---
### Subscription Plan Limits
- Plan limit checks moved from POST (after form submission) to GET (when the "New" button is clicked). Users are now redirected with a clear message before filling out a form they can't submit.
- Applies to: Quotes, Jobs, Customers, Company Users, Catalog Items.
- Job Details photo upload button is disabled with a tooltip when the photo limit is reached, showing current usage (e.g. "3 / 5 photos used").
---
### PDF / Quote
- Rush charge now appears as a line item in the Quote PDF between Discount and Tax, styled in orange to match the on-screen display.
-88
View File
@@ -1,88 +0,0 @@
BEGIN TRANSACTION;
GO
DECLARE @var0 sysname;
SELECT @var0 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[CompanyOperatingCosts]') AND [c].[name] = N'AdminOverheadPercentage');
IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [CompanyOperatingCosts] DROP CONSTRAINT [' + @var0 + '];');
ALTER TABLE [CompanyOperatingCosts] DROP COLUMN [AdminOverheadPercentage];
GO
DECLARE @var1 sysname;
SELECT @var1 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[CompanyOperatingCosts]') AND [c].[name] = N'ElectricityRatePerKwh');
IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [CompanyOperatingCosts] DROP CONSTRAINT [' + @var1 + '];');
ALTER TABLE [CompanyOperatingCosts] DROP COLUMN [ElectricityRatePerKwh];
GO
DECLARE @var2 sysname;
SELECT @var2 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[CompanyOperatingCosts]') AND [c].[name] = N'FacilityCostPercentage');
IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [CompanyOperatingCosts] DROP CONSTRAINT [' + @var2 + '];');
ALTER TABLE [CompanyOperatingCosts] DROP COLUMN [FacilityCostPercentage];
GO
DECLARE @var3 sysname;
SELECT @var3 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[CompanyOperatingCosts]') AND [c].[name] = N'GasRatePerUnit');
IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [CompanyOperatingCosts] DROP CONSTRAINT [' + @var3 + '];');
ALTER TABLE [CompanyOperatingCosts] DROP COLUMN [GasRatePerUnit];
GO
DECLARE @var4 sysname;
SELECT @var4 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[CompanyOperatingCosts]') AND [c].[name] = N'OvertimeLaborRate');
IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [CompanyOperatingCosts] DROP CONSTRAINT [' + @var4 + '];');
ALTER TABLE [CompanyOperatingCosts] DROP COLUMN [OvertimeLaborRate];
GO
DECLARE @var5 sysname;
SELECT @var5 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[CompanyOperatingCosts]') AND [c].[name] = N'SpecializedLaborRate');
IF @var5 IS NOT NULL EXEC(N'ALTER TABLE [CompanyOperatingCosts] DROP CONSTRAINT [' + @var5 + '];');
ALTER TABLE [CompanyOperatingCosts] DROP COLUMN [SpecializedLaborRate];
GO
EXEC sp_rename N'[CompanyOperatingCosts].[WaterRatePerUnit]', N'RushChargeFixedAmount', N'COLUMN';
GO
ALTER TABLE [CompanyOperatingCosts] ADD [RushChargeType] nvarchar(20) NOT NULL DEFAULT N'Percentage';
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-02-11T19:55:20.0897507Z'
WHERE [Id] = 1;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-02-11T19:55:20.0897511Z'
WHERE [Id] = 2;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-02-11T19:55:20.0897513Z'
WHERE [Id] = 3;
SELECT @@ROWCOUNT;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260211195523_UpdateOperatingCostsRushCharge', N'8.0.11');
GO
COMMIT;
GO
-1
View File
@@ -1 +0,0 @@

-745
View File
@@ -1,745 +0,0 @@
# Security Fixes Summary
This document summarizes all security vulnerabilities that were identified and fixed in the Powder Coating App.
**Date**: February 14, 2026
**Security Audit**: 18 issues identified across 4 severity levels
**Status**: CRITICAL (3/3) ✅ | HIGH (4/4) ✅ | MEDIUM (6/8) ✅ | LOW (3/3) ✅
---
## CRITICAL Priority Fixes (All Complete) ✅
### 1. Missing Authorization on Company Settings ✅
**Issue**: `CompanySettingsController` had authorization policy temporarily removed for debugging, allowing unauthorized access to sensitive company configuration.
**Fix**:
- **File**: `src/PowderCoating.Web/Controllers/CompanySettingsController.cs`
- **Action**: Restored `[Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)]` attribute
- **Impact**: Only Company Admins and SuperAdmins can now access company settings
```csharp
// BEFORE
[Authorize] // Temporarily removed CompanyAdminOnly policy for debugging
// AFTER
[Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)]
public class CompanySettingsController : Controller
```
---
### 2. Overly Permissive CORS Policy ✅
**Issue**: API allowed all origins (`AllowAnyOrigin()`) which enables CSRF attacks and unauthorized API access.
**Fix**:
- **File**: `src/PowderCoating.Api/Program.cs`
- **Action**: Restricted CORS to configuration-based whitelist
- **Configuration**: `appsettings.json` > `CorsSettings:AllowedOrigins`
```csharp
// BEFORE
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
// AFTER
var allowedOrigins = builder.Configuration.GetSection("CorsSettings:AllowedOrigins").Get<string[]>()
?? new[] { "http://localhost:3000" };
policy.WithOrigins(allowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
```
**Configuration**:
```json
{
"CorsSettings": {
"AllowedOrigins": [
"http://localhost:3000",
"http://localhost:5173"
]
}
}
```
---
### 3. Hardcoded Secrets in Configuration Files ✅
**Issue**: Production JWT secret keys and database connection strings were committed to source control in `appsettings.json`.
**Fix**:
- **Files**:
- `src/PowderCoating.Web/appsettings.json`
- `src/PowderCoating.Api/appsettings.json`
- `src/PowderCoating.Web/appsettings.Development.json` (created)
- `src/PowderCoating.Api/appsettings.Development.json` (created)
- **Actions**:
1. Replaced all production secrets with placeholders: `USE_USER_SECRETS_OR_ENVIRONMENT_VARIABLE`
2. Created separate `appsettings.Development.json` files with actual dev values
3. Updated `.gitignore` (if needed) to never commit production secrets
**Before**:
```json
{
"ConnectionStrings": {
"DefaultConnection": "Server=PROD_SERVER;Database=...;Password=RealPassword;"
},
"JwtSettings": {
"SecretKey": "CHANGE-THIS-TO-YOUR-OWN-SECRET-KEY-AT-LEAST-32-CHARACTERS"
}
}
```
**After (Production)**:
```json
{
"ConnectionStrings": {
"DefaultConnection": "USE_USER_SECRETS_OR_ENVIRONMENT_VARIABLE"
},
"JwtSettings": {
"SecretKey": "USE_USER_SECRETS_OR_ENVIRONMENT_VARIABLE",
"ExpirationMinutes": 15
}
}
```
**After (Development)**:
```json
{
"ConnectionStrings": {
"DefaultConnection": "Server=.\\SQLEXPRESS;Database=PowderCoatingDb;Trusted_Connection=true;..."
},
"JwtSettings": {
"SecretKey": "DEV-ONLY-SecretKey-MinimumLength32CharactersRequired!@#$",
"ExpirationMinutes": 15
}
}
```
---
## HIGH Priority Fixes (All Complete) ✅
### 4. Weak Password Policy ✅
**Issue**: Password requirements were too lenient (8 characters, no special characters required).
**Fix**:
- **File**: `src/PowderCoating.Web/Program.cs`
- **Actions**:
- Increased minimum length from 8 to 12 characters
- Required special characters (`RequireNonAlphanumeric = true`)
- Required 4 unique characters (`RequiredUniqueChars = 4`)
- Enabled account lockout after 5 failed attempts (15-minute lockout)
```csharp
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true; // SECURITY: Require special characters
options.Password.RequiredLength = 12; // SECURITY: Increased from 8 to 12
options.Password.RequiredUniqueChars = 4; // SECURITY: Require variety
// Account lockout for brute force protection
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
```
---
### 5. Path Traversal Vulnerability in Diagnostics ✅
**Issue**: `DiagnosticsController.ViewLogs()` allowed arbitrary file access via path traversal (`../../../../etc/passwd`).
**Fix**:
- **File**: `src/PowderCoating.Web/Controllers/DiagnosticsController.cs`
- **Actions**:
1. Added regex validation to only allow safe filenames (`[a-zA-Z0-9\-_]+\.txt`)
2. Enhanced path resolution checks to prevent traversal
3. Added security logging for attempted attacks
```csharp
// SECURITY: Sanitize filename - only allow alphanumeric, hyphens, underscores, and .txt extension
if (!System.Text.RegularExpressions.Regex.IsMatch(fileName, @"^[a-zA-Z0-9\-_]+\.txt$"))
{
_logger.LogWarning("SECURITY: Invalid log filename requested: {FileName} by {User}", fileName, User.Identity?.Name);
model.Error = "Invalid file name. Only .txt log files are allowed.";
return View(model);
}
// SECURITY: Enhanced path traversal protection
var fullPath = Path.GetFullPath(filePath);
var basePath = Path.GetFullPath(logsPath);
if (!fullPath.StartsWith(basePath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) &&
fullPath != basePath)
{
_logger.LogWarning("SECURITY: Path traversal attempt detected: {FilePath} by {User}", fullPath, User.Identity?.Name);
model.Error = "Invalid file path.";
return View(model);
}
// Verify file extension
if (Path.GetExtension(fullPath) != ".txt")
{
_logger.LogWarning("SECURITY: Non-txt file access attempted: {FilePath} by {User}", fullPath, User.Identity?.Name);
model.Error = "Only .txt files are allowed.";
return View(model);
}
```
---
### 6. IDOR on Profile Photos ✅
**Issue**: `ProfileController.Photo(string? id)` allowed any authenticated user to view any other user's profile photo without authorization check.
**Fix**:
- **File**: `src/PowderCoating.Web/Controllers/ProfileController.cs`
- **Actions**:
1. Added authorization check: user must be requesting their own photo, be a SuperAdmin, or be in the same company
2. Added security logging for unauthorized access attempts
```csharp
[HttpGet]
public async Task<IActionResult> Photo(string? id = null)
{
ApplicationUser? user;
var currentUser = await _userManager.GetUserAsync(User);
if (string.IsNullOrEmpty(id))
{
// No ID provided - use current user's photo
user = currentUser;
}
else
{
// SECURITY: Only allow access if same user, SuperAdmin, or same company
if (currentUser?.Id != id && !User.IsInRole("SuperAdmin"))
{
var requestedUser = await _userManager.FindByIdAsync(id);
// Deny access if user not found or different company
if (requestedUser == null || requestedUser.CompanyId != currentUser?.CompanyId)
{
_logger.LogWarning("SECURITY: Unauthorized photo access attempt. User {CurrentUserId} tried to access photo for {RequestedUserId}",
currentUser?.Id, id);
return Forbid();
}
}
user = await _userManager.FindByIdAsync(id);
}
// ... rest of method
}
```
---
### 7. Error Handling Exposes Stack Traces ✅
**Issue**: Error pages in production could potentially expose sensitive stack trace information.
**Status**: ✅ **Already Secure**
**Verification**:
- **File**: `src/PowderCoating.Web/Views/Home/Error.cshtml`
- Error view only shows generic error message and TraceIdentifier
- Stack traces are logged server-side but never displayed to users
- Development-specific hints are only shown when `ASPNETCORE_ENVIRONMENT=Development`
**No changes needed** - error handling was already implemented securely.
---
## MEDIUM Priority Fixes (6 of 8 Complete) ✅
### 8. Missing Security Headers ✅
**Issue**: Application did not send security headers to prevent common attacks (clickjacking, XSS, MIME sniffing, etc.).
**Fix**:
- **File**: `src/PowderCoating.Web/Program.cs`
- **Actions**: Added middleware to inject security headers on every response
```csharp
// SECURITY: Add security headers middleware
app.Use(async (context, next) =>
{
// Prevent clickjacking
context.Response.Headers.Append("X-Frame-Options", "DENY");
// Prevent MIME type sniffing
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
// Enable XSS protection (for older browsers)
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
// Strict Transport Security (HSTS) - enforce HTTPS
if (context.Request.IsHttps)
{
context.Response.Headers.Append("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
}
// Content Security Policy - restrict resource loading
context.Response.Headers.Append("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; " +
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net; " +
"img-src 'self' data: https:; " +
"connect-src 'self'");
// Referrer Policy - control referrer information
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
// Permissions Policy - disable unnecessary browser features
context.Response.Headers.Append("Permissions-Policy",
"geolocation=(), microphone=(), camera=(), payment=()");
await next();
});
```
**Headers Applied**:
- `X-Frame-Options: DENY` - Prevents iframe embedding (clickjacking protection)
- `X-Content-Type-Options: nosniff` - Prevents MIME type sniffing
- `X-XSS-Protection: 1; mode=block` - Legacy XSS filter for old browsers
- `Strict-Transport-Security` - Forces HTTPS for 1 year
- `Content-Security-Policy` - Controls what resources can be loaded
- `Referrer-Policy` - Limits referrer information leakage
- `Permissions-Policy` - Disables geolocation, camera, microphone, payment APIs
---
### 9. Excessive JWT Token Expiration ✅
**Issue**: JWT tokens were valid for 24 hours (1440 minutes), increasing risk if stolen.
**Fix**:
- **Files**:
- `src/PowderCoating.Api/appsettings.json`
- `src/PowderCoating.Api/appsettings.Development.json`
- **Actions**: Reduced expiration from 1440 minutes (24 hours) to 15 minutes
```json
{
"JwtSettings": {
"ExpirationMinutes": 15, // Changed from 1440
"RefreshTokenExpirationDays": 7
}
}
```
**Note**: Refresh token implementation already exists in configuration (7-day expiration) for seamless token renewal.
---
### 10. No Rate Limiting ⏳
**Issue**: API endpoints lack rate limiting, making them vulnerable to brute-force and DDoS attacks.
**Status**: ⏳ **Deferred** (Requires additional NuGet package)
**Recommendation**: Install `AspNetCoreRateLimit` package and configure:
```bash
dotnet add package AspNetCoreRateLimit
```
**Suggested Configuration**:
```csharp
// Program.cs
builder.Services.AddMemoryCache();
builder.Services.Configure<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting"));
builder.Services.AddInMemoryRateLimiting();
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
// appsettings.json
{
"IpRateLimiting": {
"EnableEndpointRateLimiting": true,
"GeneralRules": [
{
"Endpoint": "*",
"Period": "1m",
"Limit": 60
},
{
"Endpoint": "*/api/auth/login",
"Period": "15m",
"Limit": 5
}
]
}
}
```
---
### 11. Predictable File Upload Names ✅
**Issue**: Job photos used sequential numbering (1.jpg, 2.jpg, 3.jpg) which allows enumeration attacks.
**Fix**:
- **Files**:
- `src/PowderCoating.Application/Services/JobPhotoService.cs`
- `src/PowderCoating.Application/Interfaces/IJobPhotoService.cs`
- **Actions**:
1. Changed filename generation from sequential numbers to GUIDs
2. Removed `GetNextPhotoNumberAsync()` method
3. Updated interface documentation
```csharp
// BEFORE
var photoNumber = await GetNextPhotoNumberAsync(jobId, companyId);
var fileName = $"{photoNumber}{extension}";
// Results in: 1.jpg, 2.jpg, 3.jpg (predictable)
// AFTER
// SECURITY: Use GUID for filename to prevent enumeration attacks
var fileName = $"{Guid.NewGuid()}{extension}";
// Results in: 3fa85f64-5717-4562-b3fc-2c963f66afa6.jpg (unpredictable)
```
**Impact**: Attackers can no longer guess photo filenames by incrementing numbers.
---
### 12. Legacy ProfilePictureData Field ⏳
**Issue**: Old database byte[] storage (`ProfilePictureData`, `ProfilePictureContentType`) still exists even though photos are now stored in filesystem.
**Status**: ⏳ **Deferred** (Requires data migration)
**Recommendation**:
1. Create migration script to migrate any remaining database photos to filesystem
2. Create EF migration to drop old columns:
```csharp
migrationBuilder.DropColumn(name: "ProfilePictureData", table: "AspNetUsers");
migrationBuilder.DropColumn(name: "ProfilePictureContentType", table: "AspNetUsers");
```
3. Update `ApplicationUser` entity to remove properties
4. Remove fallback logic from `ProfileController.Photo()`
**Risk**: Low (backward compatibility fallback rarely used, all new uploads go to filesystem)
---
### 13. Missing CSRF Tokens on AJAX Endpoints ⏳
**Issue**: Some AJAX endpoints may not validate anti-forgery tokens.
**Status**: ⏳ **Partially Fixed**
**Current Status**: Most AJAX endpoints already use `[ValidateAntiForgeryToken]` attribute.
**Verification Needed**: Audit all POST/PUT/DELETE endpoints to ensure they validate CSRF tokens.
**Example of Correct Implementation**:
```csharp
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileDto dto)
{
// AJAX call must include anti-forgery token in headers
}
```
**Client-side** (already implemented):
```javascript
fetch('/Profile/UpdateProfile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('[name="__RequestVerificationToken"]').value
},
body: JSON.stringify(data)
});
```
---
### 14. Input Validation on Search Terms ✅
**Issue**: Search terms were not sanitized, potentially allowing SQL injection or XSS attacks.
**Fix**:
- **File**: `src/PowderCoating.Web/Helpers/SecurityHelper.cs` (created)
- **Updated**: `src/PowderCoating.Web/Controllers/CustomersController.cs` (example)
- **Actions**:
1. Created `SecurityHelper` class with multiple validation methods
2. Applied `SanitizeSearchTerm()` to all search inputs
**SecurityHelper Methods**:
```csharp
public static class SecurityHelper
{
// Sanitizes search terms (removes dangerous chars, limits length)
public static string? SanitizeSearchTerm(string? searchTerm);
// Validates alphanumeric-safe strings
public static bool IsAlphanumericSafe(string? input, bool allowSpaces = false);
// Validates file extensions
public static bool HasSafeFileExtension(string fileName, string[] allowedExtensions);
// Sanitizes filenames
public static string SanitizeFileName(string fileName);
// Validates paths (anti-traversal)
public static bool IsPathWithinBase(string basePath, string filePath);
}
```
**Usage**:
```csharp
// BEFORE
public async Task<IActionResult> Index(string? searchTerm, ...)
{
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var search = searchTerm.ToLower(); // Unsafe!
}
}
// AFTER
using PowderCoating.Web.Helpers;
public async Task<IActionResult> Index(string? searchTerm, ...)
{
// SECURITY: Sanitize search input to prevent injection attacks
searchTerm = SecurityHelper.SanitizeSearchTerm(searchTerm);
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var search = searchTerm.ToLower(); // Now safe
}
}
```
**Protection Against**:
- SQL Injection (removes `;'--` and other SQL chars)
- XSS (removes `<>` script tags)
- Command Injection (removes `&|()` shell chars)
- Length-based DoS (limits to 100 chars)
**Action Required**: Apply `SecurityHelper.SanitizeSearchTerm()` to all controllers with search functionality:
- ✅ CustomersController
- ⏳ JobsController
- ⏳ QuotesController
- ⏳ InventoryController
- ⏳ EquipmentController
- ⏳ AppointmentsController
- ⏳ SuppliersController
- ⏳ MaintenanceController
- ⏳ CatalogItemsController
- ⏳ ShopWorkersController
- ⏳ CompanyUsersController
- ⏳ PlatformUsersController
- ⏳ DiagnosticsController
---
## LOW Priority Fixes (All Complete) ✅
### 15. Overly Permissive AllowedHosts ✅
**Issue**: N/A - `AllowedHosts` was already properly configured.
**Status**: ✅ **Already Secure**
**Current Configuration**:
```json
{
"AllowedHosts": "localhost;127.0.0.1" // Development
}
```
**Production**: User should update to actual domain(s):
```json
{
"AllowedHosts": "yourapp.com;www.yourapp.com"
}
```
---
### 16. Insecure Session Cookie Configuration ✅
**Issue**: Session cookies lacked secure configuration (SameSite, SecurePolicy).
**Fix**:
- **File**: `src/PowderCoating.Web/Program.cs`
- **Actions**: Enhanced session cookie security
```csharp
// BEFORE
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
// AFTER
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true; // Prevent JavaScript access
options.Cookie.IsEssential = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // SECURITY: Require HTTPS
options.Cookie.SameSite = SameSiteMode.Strict; // SECURITY: Prevent CSRF
options.Cookie.Name = ".PowderCoating.Session"; // Custom name (less predictable)
});
```
**Protections**:
- `HttpOnly = true` - Prevents JavaScript from reading cookie (XSS protection)
- `SecurePolicy = Always` - Cookie only sent over HTTPS
- `SameSite = Strict` - Prevents CSRF attacks
- Custom cookie name - Less predictable than default `.AspNetCore.Session`
---
### 17. Missing Security Event Logging ✅
**Issue**: Security events (unauthorized access, path traversal attempts, etc.) were not being logged.
**Status**: ✅ **Fixed During Other Fixes**
**Logging Added**:
- Path traversal attempts (`DiagnosticsController.ViewLogs()`)
- Unauthorized photo access attempts (`ProfileController.Photo()`)
- Invalid filename requests (`DiagnosticsController.ViewLogs()`)
**Example**:
```csharp
_logger.LogWarning("SECURITY: Path traversal attempt detected: {FilePath} by {User}",
fullPath, User.Identity?.Name);
_logger.LogWarning("SECURITY: Unauthorized photo access attempt. User {CurrentUserId} tried to access photo for {RequestedUserId}",
currentUser?.Id, id);
```
**Logs Location**:
- Development: `logs/errors-{date}.txt`
- Production: Serilog configured to write to file and console (can integrate with Application Insights, Seq, etc.)
---
## Summary
### Fixes by Priority
| Priority | Total | Complete | Deferred | Notes |
|----------|-------|----------|----------|-------|
| **CRITICAL** | 3 | 3 ✅ | 0 | All critical issues resolved |
| **HIGH** | 4 | 4 ✅ | 0 | All high-priority issues resolved |
| **MEDIUM** | 8 | 6 ✅ | 2 ⏳ | Rate limiting and CSRF audit deferred |
| **LOW** | 3 | 3 ✅ | 0 | All low-priority issues resolved |
| **TOTAL** | **18** | **16 ✅** | **2 ⏳** | **89% complete** |
### Deferred Items (Non-Critical)
1. **Rate Limiting** (MEDIUM)
- Requires installing `AspNetCoreRateLimit` NuGet package
- Recommended for production, but not blocking deployment
2. **CSRF Token Audit** (MEDIUM)
- Most endpoints already validate tokens
- Recommend full audit to verify all POST/PUT/DELETE endpoints
3. **Legacy ProfilePictureData Removal** (MEDIUM)
- Low risk (fallback rarely used)
- Requires data migration before dropping columns
---
## Files Modified
### Configuration Files
- ✅ `src/PowderCoating.Web/appsettings.json` - Removed secrets, added placeholders
- ✅ `src/PowderCoating.Web/appsettings.Development.json` - Created with dev values
- ✅ `src/PowderCoating.Api/appsettings.json` - Removed secrets, reduced JWT expiration
- ✅ `src/PowderCoating.Api/appsettings.Development.json` - Created with dev values
### Application Code
- ✅ `src/PowderCoating.Web/Program.cs` - Security headers, session cookies, password policy
- ✅ `src/PowderCoating.Api/Program.cs` - CORS restriction
- ✅ `src/PowderCoating.Web/Controllers/CompanySettingsController.cs` - Authorization restored
- ✅ `src/PowderCoating.Web/Controllers/DiagnosticsController.cs` - Path traversal fixes
- ✅ `src/PowderCoating.Web/Controllers/ProfileController.cs` - IDOR fix
- ✅ `src/PowderCoating.Web/Controllers/CustomersController.cs` - Input sanitization
- ✅ `src/PowderCoating.Application/Services/JobPhotoService.cs` - GUID filenames
- ✅ `src/PowderCoating.Application/Interfaces/IJobPhotoService.cs` - Updated interface
### New Files Created
- ✅ `src/PowderCoating.Web/Helpers/SecurityHelper.cs` - Input validation utilities
- ✅ `DEPLOYMENT_CONFIGURATION.md` - Comprehensive deployment guide
- ✅ `SECURITY_FIXES_SUMMARY.md` - This document
---
## Next Steps for Deployment
### Development Environment
1. ✅ All changes applied - ready to use
2. ✅ Configuration in `appsettings.Development.json`
3. ✅ Test all functionality to verify fixes don't break existing features
### Production Deployment
1. ⚠️ **DO NOT deploy** until you configure production secrets
2. 📖 **READ**: `DEPLOYMENT_CONFIGURATION.md` for step-by-step instructions
3. 🔐 Set environment variables for:
- `ConnectionStrings__DefaultConnection`
- `JwtSettings__SecretKey`
- `CorsSettings__AllowedOrigins` (update to production domains)
- `AllowedHosts` (update to production domain)
4. ✅ Enable HTTPS with valid SSL certificate
5. ✅ Run database migrations on production database
6. ✅ Test all critical paths after deployment
7. 📊 Configure monitoring and alerting (Application Insights recommended)
---
## Security Best Practices Going Forward
1. **Never commit secrets** - Always use environment variables or Key Vault
2. **Rotate secrets regularly** - JWT keys and DB passwords every 90 days
3. **Monitor security logs** - Watch for attack patterns in `errors-{date}.txt`
4. **Keep dependencies updated** - Run `dotnet list package --outdated` monthly
5. **Regular security audits** - Re-run this checklist quarterly
6. **Use HTTPS everywhere** - Never deploy without SSL certificate
7. **Apply all Windows/SQL Server patches** - Enable automatic updates
8. **Backup databases daily** - Test restore procedures monthly
---
## Testing Checklist
Before deploying to production, verify:
- [ ] All unit tests pass (`dotnet test`)
- [ ] Login works with new 12-character password requirement
- [ ] Company Settings page requires Company Admin role
- [ ] API CORS blocks unauthorized origins
- [ ] Profile photos enforce same-company access restriction
- [ ] Diagnostics log viewer prevents path traversal
- [ ] Job photo uploads use GUID filenames
- [ ] Session cookies are secure and SameSite=Strict
- [ ] Security headers appear in browser DevTools (Network tab)
- [ ] Search terms are sanitized (test with `<script>alert('xss')</script>`)
- [ ] JWT tokens expire after 15 minutes
---
**Document Version**: 1.0
**Last Updated**: February 14, 2026
**Security Audit Completion**: 89% (16 of 18 issues resolved)
-111
View File
@@ -1,111 +0,0 @@
# How to Fix Seed Data Duplicate Errors
## Problem
You're getting duplicate key errors because seed data was partially inserted, and when you try to seed again, it conflicts with existing data.
## Quick Fix (5 minutes)
### Step 1: Connect to Database
1. Open SQL Server Management Studio (SSMS)
2. Connect to your testing server database
3. Select the `PowderCoatingDb` database
### Step 2: Clean Up Existing Seed Data
1. Open the file: `cleanup-seed-data.sql`
2. **IMPORTANT:** On line 9, change `@CompanyId` to your actual company ID
```sql
DECLARE @CompanyId INT = 1 -- Change to your company ID
```
3. Execute the entire script
4. Verify it shows "0 Remaining Records" for all tables
### Step 3: Re-seed via Web Interface
1. Log in to your application as SuperAdmin
2. Navigate to: **Platform Management → Seed Data** (or `/SeedData`)
3. Click **"Seed Company Data"** for your company
4. All data should seed successfully now
## Finding Your Company ID
Run this query in SSMS:
```sql
SELECT Id, CompanyName, CompanyCode FROM Companies WHERE IsDeleted = 0
```
## What Gets Deleted
The cleanup script removes:
- ✓ All customers
- ✓ All inventory items
- ✓ All equipment
- ✓ All jobs and quotes
- ✓ All catalog items
- ✓ All pricing tiers
- ✓ Operating costs
## What Gets Preserved
- ✓ Your company record
- ✓ All user accounts
- ✓ System roles
- ✓ You can still log in
## Why This Happens
The seed data checks if "any" records exist, but if:
1. Seeding fails partway through (permission error, network issue, etc.)
2. Then you try to seed again
3. It tries to insert the same data again → duplicate key errors
## Prevention
After I update the seed service with better validation, it will:
- Check for specific seed data patterns, not just "any" data
- Validate company codes before creating SKUs
- Give clear error messages if data is partially seeded
## Alternative: Reset Everything (Nuclear Option)
If you want a completely fresh database:
```sql
-- WARNING: Deletes ALL data including users (except SuperAdmin)
USE PowderCoatingDb
GO
-- Keep a list of superadmins
SELECT * INTO #TempAdmins FROM AspNetUsers WHERE Email IN ('superadmin@powdercoating.com', 'admin@powdercoating.com')
-- Drop and recreate (or just delete all data)
DELETE FROM JobPhotos
DELETE FROM JobNotes
DELETE FROM JobItems
DELETE FROM Jobs
DELETE FROM QuoteItems
DELETE FROM Quotes
DELETE FROM InventoryTransactions
DELETE FROM InventoryItems
DELETE FROM Equipment
DELETE FROM MaintenanceRecords
DELETE FROM CatalogItems
DELETE FROM Customers
DELETE FROM PricingTiers
DELETE FROM CompanyOperatingCosts
-- Delete non-superadmin users
DELETE FROM AspNetUserRoles WHERE UserId NOT IN (SELECT Id FROM #TempAdmins)
DELETE FROM AspNetUsers WHERE Id NOT IN (SELECT Id FROM #TempAdmins)
-- Keep only one company
DELETE FROM Companies WHERE CompanyCode != 'DEMO'
DROP TABLE #TempAdmins
-- Now go to the web interface and seed system data, then seed company data
```
## Need Help?
Check the logs at:
- `[App-Folder]\logs\errors-YYYYMMDD.txt` for today's errors
- Look for specific duplicate key errors to know which table is failing
-351
View File
@@ -1,351 +0,0 @@
# Troubleshooting: App Not Starting
## 🐛 Problem: "Nothing happens when I run the app"
This usually means the application is hanging during startup, most likely during database migration or seeding.
---
## 🔍 Quick Diagnostics
### Step 1: Check What's in the Console
When you run `dotnet run`, you should see output. What do you see?
#### ✅ GOOD - You should see this:
```
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7001
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
```
#### ❌ BAD - If you see this (or nothing):
```
Building...
[Hangs here - nothing else appears]
```
This means it's **stuck during startup**, likely during database migration.
---
## 🛠️ Solution 1: Skip Automatic Migrations (Recommended)
The issue is that the app tries to migrate the database on startup. Let's disable that temporarily.
### Update Program.cs
**Comment out the automatic migration section:**
Find this section in `src/PowderCoating.Web/Program.cs` (around line 112-133):
```csharp
// Seed database with initial data
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<ApplicationDbContext>();
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
// Run migrations
await context.Database.MigrateAsync();
// Seed roles and admin user
await SeedData.InitializeAsync(services, userManager, roleManager);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while migrating or seeding the database.");
}
}
```
**Replace it with:**
```csharp
// NOTE: Database seeding is disabled for now
// Run migrations manually with: dotnet ef database update
// Seeding will happen on first database update
// Uncomment below after you've created the database manually:
/*
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
// Seed roles and admin user (no migration here)
await SeedData.InitializeAsync(services, userManager, roleManager);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}
*/
```
### Then Manually Create the Database
```bash
cd src/PowderCoating.Web
# Create migration
dotnet ef migrations add InitialCreate --project ../PowderCoating.Infrastructure
# Apply migration
dotnet ef database update --project ../PowderCoating.Infrastructure
```
### Now Run the App Again
```bash
dotnet run
```
It should start immediately!
---
## 🛠️ Solution 2: Check SQL Server Connection
If the app is hanging, it might be trying to connect to SQL Server but failing.
### Is SQL Server Running?
**Windows - Check Service:**
```powershell
Get-Service MSSQL$SQLEXPRESS
# If not running:
Start-Service MSSQL$SQLEXPRESS
```
**Or use Services.msc:**
1. Press `Win + R`
2. Type `services.msc`
3. Find "SQL Server (SQLEXPRESS)"
4. Check if it's running
### Test Connection String
The app uses this connection string (in `appsettings.json`):
```json
"Server=.\\SQLEXPRESS;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true;TrustServerCertificate=true"
```
**Test it manually:**
1. Open SQL Server Management Studio (SSMS) or Azure Data Studio
2. Connect to: `.\SQLEXPRESS`
3. If you can connect, SQL Server is running
If you **can't connect**, SQL Express might not be installed.
---
## 🛠️ Solution 3: Use LocalDB Instead
If SQL Express is giving you trouble, switch to LocalDB:
### Update Connection String
Edit `src/PowderCoating.Web/appsettings.json`:
**Change from:**
```json
"ConnectionStrings": {
"DefaultConnection": "Server=.\\SQLEXPRESS;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true;TrustServerCertificate=true"
}
```
**To:**
```json
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true"
}
```
Also update `src/PowderCoating.Api/appsettings.json` the same way.
### Create Database
```bash
dotnet ef database update --project src/PowderCoating.Infrastructure --startup-project src/PowderCoating.Web
```
### Run the App
```bash
cd src/PowderCoating.Web
dotnet run
```
---
## 🛠️ Solution 4: Verbose Logging
Let's see exactly where it's hanging.
### Run with Detailed Logging
```bash
cd src/PowderCoating.Web
dotnet run --verbosity diagnostic
```
This will show you EXACTLY where it stops.
### Check the Log File
Look in: `logs/powdercoating-YYYYMMDD.txt`
The last line before it hangs will tell you what's wrong.
---
## 🛠️ Solution 5: Complete Clean Rebuild
Sometimes old build artifacts cause issues.
```bash
# From solution root
dotnet clean
rm -rf **/bin **/obj
# Restore packages
dotnet restore
# Build
dotnet build
# Run
cd src/PowderCoating.Web
dotnet run
```
---
## 📋 Diagnostic Checklist
Work through these in order:
- [ ] **Check console output** - What's the last line you see?
- [ ] **Is SQL Server running?** - Check the service
- [ ] **Can you connect to SQL Server?** - Try SSMS/Azure Data Studio
- [ ] **Comment out auto-migration** - Use Solution 1 above
- [ ] **Create database manually** - Use `dotnet ef database update`
- [ ] **Try running again** - Does it work now?
- [ ] **Check logs folder** - Look at the log file
- [ ] **Try LocalDB** - If SQL Express isn't working
---
## 🎯 Most Likely Issue
**90% of the time, the issue is:**
The app is trying to connect to SQL Express but:
1. SQL Express isn't running, OR
2. SQL Express isn't installed, OR
3. The connection string is wrong
**Quick fix:**
1. Comment out the auto-migration code (Solution 1)
2. Use LocalDB instead (Solution 3)
3. Run the app - it should start immediately
4. Create the database manually with `dotnet ef database update`
---
## 💡 Expected Behavior
### When Working Correctly:
```bash
$ cd src/PowderCoating.Web
$ dotnet run
Building...
Build succeeded.
0 Warning(s)
0 Error(s)
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7001
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Projects\PowderCoatingApp\src\PowderCoating.Web
```
**Then:** Open browser to https://localhost:7001 and you see the home page.
---
## 🆘 Still Not Working?
Share these details:
1. **What OS are you on?** (Windows, Mac, Linux)
2. **What's the last line you see** when running `dotnet run`?
3. **Is SQL Server installed?** How did you install it?
4. **What's in the log file?** (`logs/powdercoating-*.txt`)
5. **Does this work?** `dotnet ef --version`
With these details, I can provide more specific help!
---
## ✅ Quick Win
**Try this right now:**
1. **Stop the app** if it's running (Ctrl+C)
2. **Edit Program.cs** - Comment out lines 112-133 (the entire seeding block)
3. **Run the app:**
```bash
dotnet run
```
4. **You should see:**
```
Now listening on: https://localhost:7001
```
5. **Open browser** to https://localhost:7001
6. **You'll see an error** about missing database - that's OK!
7. **Stop the app** (Ctrl+C)
8. **Create database:**
```bash
dotnet ef database update --project ../PowderCoating.Infrastructure
```
9. **Run again:**
```bash
dotnet run
```
10. **Now it should work!** Navigate to https://localhost:7001/Identity/Account/Login
This bypasses the automatic migration that's causing the hang.
-300
View File
@@ -1,300 +0,0 @@
# AutoMapper 16.0 Update Summary
## ✅ Changes Completed
### 1. Package Updates
Updated AutoMapper packages from version 13.0.1 to **16.0.0** in:
-`PowderCoating.Application/PowderCoating.Application.csproj`
-`PowderCoating.Web/PowderCoating.Web.csproj`
-`PowderCoating.Api/PowderCoating.Api.csproj`
### 2. AutoMapper Profile Classes Created
Created complete AutoMapper mapping profiles:
#### **CustomerProfile.cs** (`src/PowderCoating.Application/Mappings/CustomerProfile.cs`)
- ✅ Customer → CustomerDto
- ✅ CreateCustomerDto → Customer
- ✅ UpdateCustomerDto → Customer
- ✅ Customer → CustomerListDto (with formatted contact name)
#### **JobProfile.cs** (`src/PowderCoating.Application/Mappings/JobProfile.cs`)
- ✅ Job → JobDto (with related entities)
- ✅ CreateJobDto → Job
- ✅ UpdateJobDto → Job
- ✅ Job → JobListDto
- ✅ JobItem → JobItemDto
- ✅ CreateJobItemDto → JobItem
- ✅ Job → ShopFloorJobDto (with priority colors and next steps)
- ✅ Helper methods for:
- Priority color coding
- Next step suggestions based on status
- Enum name formatting (e.g., "InPreparation" → "In Preparation")
### 3. Documentation Added
-`AUTOMAPPER_UPDATE.md` - Complete update guide and verification steps
## 🎯 Build Status
**Expected Result:****BUILDS SUCCESSFULLY**
The project is now ready to build with AutoMapper 16.0. All mappings are configured and no breaking changes affect our usage patterns.
## 📦 What You're Getting
### Updated Project Files (55+ files total)
```
PowderCoatingApp/
├── src/
│ ├── PowderCoating.Application/
│ │ ├── Mappings/ ← NEW FOLDER
│ │ │ ├── CustomerProfile.cs ← NEW
│ │ │ └── JobProfile.cs ← NEW
│ │ └── PowderCoating.Application.csproj (AutoMapper 16.0)
│ ├── PowderCoating.Web/
│ │ └── PowderCoating.Web.csproj (AutoMapper 16.0)
│ └── PowderCoating.Api/
│ └── PowderCoating.Api.csproj (AutoMapper 16.0)
├── AUTOMAPPER_UPDATE.md ← NEW
└── [All other original files]
```
## 🚀 How to Verify the Build
### Step 1: Extract the Archive
```bash
# Windows
Expand-Archive PowderCoatingApp.zip -DestinationPath C:\Projects\
# Mac/Linux
tar -xzf PowderCoatingApp.tar.gz -C ~/Projects/
```
### Step 2: Restore Packages
```bash
cd PowderCoatingApp
dotnet restore
```
### Step 3: Build the Solution
```bash
dotnet build
```
**Expected Output:**
```
Build succeeded.
0 Warning(s)
0 Error(s)
```
### Step 4: Verify AutoMapper Registration
The AutoMapper profiles will be automatically discovered and registered because of this line in `Program.cs`:
```csharp
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
```
This scans all assemblies and registers any classes that inherit from `Profile`.
## 🔍 What AutoMapper 16.0 Brings
### Performance Improvements
- Faster mapping operations
- Better memory efficiency
- Optimized projection queries
### Enhanced Features
- Improved null handling
- Better async support
- Enhanced source generation support
- More detailed error messages
### Compatibility
- ✅ Fully compatible with .NET 8.0
- ✅ Works with Entity Framework Core 8.0
- ✅ No breaking changes for standard usage patterns
## 📝 Example Usage in Controllers
### Customer Controller Example
```csharp
public class CustomersController : Controller
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public CustomersController(IUnitOfWork unitOfWork, IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<IActionResult> Index()
{
var customers = await _unitOfWork.Customers.GetAllAsync();
var customerDtos = _mapper.Map<List<CustomerListDto>>(customers);
return View(customerDtos);
}
public async Task<IActionResult> Details(int id)
{
var customer = await _unitOfWork.Customers.GetByIdAsync(id);
if (customer == null) return NotFound();
var customerDto = _mapper.Map<CustomerDto>(customer);
return View(customerDto);
}
[HttpPost]
public async Task<IActionResult> Create(CreateCustomerDto dto)
{
if (!ModelState.IsValid) return View(dto);
var customer = _mapper.Map<Customer>(dto);
await _unitOfWork.Customers.AddAsync(customer);
await _unitOfWork.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
```
### API Controller Example
```csharp
[ApiController]
[Route("api/[controller]")]
public class JobsController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public JobsController(IUnitOfWork unitOfWork, IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<JobListDto>>> GetAll()
{
var jobs = await _unitOfWork.Jobs.GetAllAsync();
var jobDtos = _mapper.Map<List<JobListDto>>(jobs);
return Ok(jobDtos);
}
[HttpGet("shopfloor")]
public async Task<ActionResult<IEnumerable<ShopFloorJobDto>>> GetShopFloorJobs()
{
var jobs = await _unitOfWork.Jobs
.FindAsync(j => j.Status != JobStatus.Completed &&
j.Status != JobStatus.Cancelled);
var shopFloorDtos = _mapper.Map<List<ShopFloorJobDto>>(jobs);
return Ok(shopFloorDtos);
}
}
```
## ⚠️ Important Notes
### AutoMapper Profile Discovery
The profiles are automatically discovered because they:
1. Inherit from `AutoMapper.Profile`
2. Are in an assembly that's scanned by `AddAutoMapper()`
3. Have a parameterless constructor
### Adding More Profiles
When you add new features, create new profile classes:
1. Create file in `src/PowderCoating.Application/Mappings/`
2. Inherit from `Profile`
3. Configure mappings in constructor
4. That's it! No registration needed - it's automatic
Example:
```csharp
public class InventoryProfile : Profile
{
public InventoryProfile()
{
CreateMap<InventoryItem, InventoryItemDto>();
CreateMap<CreateInventoryItemDto, InventoryItem>();
}
}
```
## 🐛 Troubleshooting
### If Build Fails with AutoMapper Errors
1. **Clear NuGet Cache:**
```bash
dotnet nuget locals all --clear
dotnet restore --force
```
2. **Verify Package Versions:**
```bash
dotnet list package | grep AutoMapper
```
Should show:
```
AutoMapper 16.0.0
AutoMapper.Extensions.Microsoft... 16.0.0
```
3. **Check for Version Conflicts:**
All AutoMapper packages must be the same version (16.0.0)
### If Mapping Fails at Runtime
Check that:
- Profile classes are in the Application project
- They inherit from `Profile`
- They have public parameterless constructors
- `AddAutoMapper()` is called in `Program.cs`
## ✨ New Features You Can Use
### Conditional Mapping
```csharp
CreateMap<Customer, CustomerDto>()
.ForMember(dest => dest.FullName,
opt => opt.Condition(src => !string.IsNullOrEmpty(src.FirstName)));
```
### Reverse Mapping
```csharp
CreateMap<Customer, CustomerDto>().ReverseMap();
```
### Projection (for EF Core queries)
```csharp
var customers = await _context.Customers
.ProjectTo<CustomerDto>(_mapper.ConfigurationProvider)
.ToListAsync();
```
## 📋 Checklist
- ✅ AutoMapper packages updated to 16.0.0
- ✅ CustomerProfile created with all mappings
- ✅ JobProfile created with all mappings
- ✅ Helper methods added for formatting and colors
- ✅ Documentation created
- ✅ Project ready to build
- ⏭️ Next: Run `dotnet build` to verify
- ⏭️ Next: Create additional profiles as you add features
## 🎉 Conclusion
Your project is now updated with **AutoMapper 16.0** and includes:
- ✅ All necessary mapping configurations
- ✅ Smart helper methods for shop floor display
- ✅ Proper formatting for enum values
- ✅ Complete documentation
**The project is ready to build and run!**
When you open the solution and build it, AutoMapper 16.0 will be restored from NuGet and all mappings will be automatically registered.
-241
View File
@@ -1,241 +0,0 @@
-- Safe Multi-Tenancy Migration Script with proper SQL Server settings
USE PowderCoatingDb;
SET ANSI_NULLS ON;
SET QUOTED_IDENTIFIER ON;
GO
PRINT '=== Starting Multi-Tenancy Migration ===';
-- Step 1: Verify Companies table was created
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Companies')
BEGIN
RAISERROR('ERROR: Companies table was not created properly!', 16, 1);
RETURN;
END
PRINT 'Companies table exists';
-- Step 2: Add CompanyId to AspNetUsers
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[AspNetUsers]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[AspNetUsers] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_AspNetUsers_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to AspNetUsers';
END
ELSE
BEGIN
PRINT 'CompanyId already exists in AspNetUsers';
END
-- Step 3: Add CompanyRole to AspNetUsers
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[AspNetUsers]') AND name = 'CompanyRole')
BEGIN
ALTER TABLE [dbo].[AspNetUsers] ADD [CompanyRole] NVARCHAR(MAX) NULL;
PRINT 'Added CompanyRole to AspNetUsers';
END
-- Step 4: Add CompanyId to all other tables
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Customers]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[Customers] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_Customers_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to Customers';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Jobs]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[Jobs] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_Jobs_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to Jobs';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[JobItems]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[JobItems] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_JobItems_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to JobItems';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Quotes]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[Quotes] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_Quotes_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to Quotes';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[QuoteItems]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[QuoteItems] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_QuoteItems_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to QuoteItems';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[InventoryItems]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[InventoryItems] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_InventoryItems_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to InventoryItems';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[InventoryTransactions]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[InventoryTransactions] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_InventoryTransactions_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to InventoryTransactions';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Equipment]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[Equipment] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_Equipment_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to Equipment';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[MaintenanceRecords]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[MaintenanceRecords] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_MaintenanceRecords_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to MaintenanceRecords';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Suppliers]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[Suppliers] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_Suppliers_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to Suppliers';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[PricingTiers]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[PricingTiers] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_PricingTiers_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to PricingTiers';
-- Update seeded pricing tiers
UPDATE [dbo].[PricingTiers] SET [CompanyId] = 1 WHERE [CompanyId] = 0;
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[JobPhotos]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[JobPhotos] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_JobPhotos_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to JobPhotos';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[JobNotes]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[JobNotes] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_JobNotes_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to JobNotes';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[CustomerNotes]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[CustomerNotes] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_CustomerNotes_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to CustomerNotes';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[JobStatusHistory]') AND name = 'CompanyId')
BEGIN
ALTER TABLE [dbo].[JobStatusHistory] ADD [CompanyId] INT NOT NULL CONSTRAINT DF_JobStatusHistory_CompanyId DEFAULT 1;
PRINT 'Added CompanyId to JobStatusHistory';
END
GO
-- Step 5: Create Indexes
PRINT '=== Creating Indexes ===';
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Customers_CompanyId')
CREATE INDEX [IX_Customers_CompanyId] ON [dbo].[Customers]([CompanyId]);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Jobs_CompanyId')
CREATE INDEX [IX_Jobs_CompanyId] ON [dbo].[Jobs]([CompanyId]);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Equipment_CompanyId')
CREATE INDEX [IX_Equipment_CompanyId] ON [dbo].[Equipment]([CompanyId]);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Quotes_CompanyId')
CREATE INDEX [IX_Quotes_CompanyId] ON [dbo].[Quotes]([CompanyId]);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_InventoryItems_CompanyId')
CREATE INDEX [IX_InventoryItems_CompanyId] ON [dbo].[InventoryItems]([CompanyId]);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Suppliers_CompanyId')
CREATE INDEX [IX_Suppliers_CompanyId] ON [dbo].[Suppliers]([CompanyId]);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_PricingTiers_CompanyId')
CREATE INDEX [IX_PricingTiers_CompanyId] ON [dbo].[PricingTiers]([CompanyId]);
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AspNetUsers_CompanyId')
CREATE INDEX [IX_AspNetUsers_CompanyId] ON [dbo].[AspNetUsers]([CompanyId]);
PRINT 'Indexes created';
GO
-- Step 6: Add Foreign Keys
PRINT '=== Adding Foreign Keys ===';
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_AspNetUsers_Companies_CompanyId')
BEGIN
ALTER TABLE [dbo].[AspNetUsers]
ADD CONSTRAINT [FK_AspNetUsers_Companies_CompanyId]
FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies]([Id]);
PRINT 'Added FK: AspNetUsers -> Companies';
END
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_Customers_Companies_CompanyId')
BEGIN
ALTER TABLE [dbo].[Customers]
ADD CONSTRAINT [FK_Customers_Companies_CompanyId]
FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies]([Id]);
PRINT 'Added FK: Customers -> Companies';
END
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_Jobs_Companies_CompanyId')
BEGIN
ALTER TABLE [dbo].[Jobs]
ADD CONSTRAINT [FK_Jobs_Companies_CompanyId]
FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies]([Id]);
PRINT 'Added FK: Jobs -> Companies';
END
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_Equipment_Companies_CompanyId')
BEGIN
ALTER TABLE [dbo].[Equipment]
ADD CONSTRAINT [FK_Equipment_Companies_CompanyId]
FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies]([Id]);
PRINT 'Added FK: Equipment -> Companies';
END
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_Quotes_Companies_CompanyId')
BEGIN
ALTER TABLE [dbo].[Quotes]
ADD CONSTRAINT [FK_Quotes_Companies_CompanyId]
FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies]([Id]);
PRINT 'Added FK: Quotes -> Companies';
END
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_InventoryItems_Companies_CompanyId')
BEGIN
ALTER TABLE [dbo].[InventoryItems]
ADD CONSTRAINT [FK_InventoryItems_Companies_CompanyId]
FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies]([Id]);
PRINT 'Added FK: InventoryItems -> Companies';
END
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_Suppliers_Companies_CompanyId')
BEGIN
ALTER TABLE [dbo].[Suppliers]
ADD CONSTRAINT [FK_Suppliers_Companies_CompanyId]
FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies]([Id]);
PRINT 'Added FK: Suppliers -> Companies';
END
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_PricingTiers_Companies_CompanyId')
BEGIN
ALTER TABLE [dbo].[PricingTiers]
ADD CONSTRAINT [FK_PricingTiers_Companies_CompanyId]
FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies]([Id]);
PRINT 'Added FK: PricingTiers -> Companies';
END
PRINT 'All foreign keys added';
GO
PRINT '';
PRINT '==============================================';
PRINT ' Multi-Tenancy Migration COMPLETE!';
PRINT '==============================================';
PRINT '';
PRINT 'Next steps:';
PRINT '1. Run: dotnet run --project src/PowderCoating.Web';
PRINT '2. Login: superadmin@powdercoating.com / SuperAdmin123!';
PRINT '3. Or login: admin@demo.com / CompanyAdmin123!';
PRINT '';
PRINT '==============================================';
GO
-194
View File
@@ -1,194 +0,0 @@
-- Safe Multi-Tenancy Migration Script
-- Run this instead of 'dotnet ef database update'
USE PowderCoatingDb;
GO
PRINT '=== Step 1: Create Companies Table ===';
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Companies')
BEGIN
CREATE TABLE Companies (
Id INT IDENTITY(1,1) PRIMARY KEY,
CompanyName NVARCHAR(MAX) NOT NULL,
CompanyCode NVARCHAR(450) NULL,
PrimaryContactName NVARCHAR(MAX) NOT NULL,
PrimaryContactEmail NVARCHAR(MAX) NOT NULL,
Phone NVARCHAR(MAX) NULL,
Address NVARCHAR(MAX) NULL,
City NVARCHAR(MAX) NULL,
State NVARCHAR(MAX) NULL,
ZipCode NVARCHAR(MAX) NULL,
IsActive BIT NOT NULL DEFAULT 1,
SubscriptionStartDate DATETIME2 NOT NULL,
SubscriptionEndDate DATETIME2 NULL,
SubscriptionPlan NVARCHAR(MAX) NULL,
TimeZone NVARCHAR(MAX) NULL,
LogoPath NVARCHAR(MAX) NULL,
Settings NVARCHAR(MAX) NULL,
CompanyId INT NOT NULL DEFAULT 0,
CreatedAt DATETIME2 NOT NULL,
UpdatedAt DATETIME2 NULL,
CreatedBy NVARCHAR(MAX) NULL,
UpdatedBy NVARCHAR(MAX) NULL,
IsDeleted BIT NOT NULL DEFAULT 0,
DeletedAt DATETIME2 NULL,
DeletedBy NVARCHAR(MAX) NULL
);
CREATE UNIQUE INDEX IX_Companies_CompanyCode ON Companies(CompanyCode) WHERE CompanyCode IS NOT NULL;
PRINT 'Companies table created';
END
GO
PRINT '=== Step 2: Insert Default Company ===';
IF NOT EXISTS (SELECT * FROM Companies WHERE Id = 1)
BEGIN
SET IDENTITY_INSERT Companies ON;
INSERT INTO Companies (
Id, CompanyName, CompanyCode, PrimaryContactName, PrimaryContactEmail,
Phone, Address, City, State, ZipCode,
IsActive, SubscriptionStartDate, SubscriptionPlan, TimeZone,
CompanyId, CreatedAt, IsDeleted
) VALUES (
1, 'Demo Company', 'DEMO', 'Admin User', 'admin@demo.com',
'(555) 123-4567', '123 Demo Street', 'Demo City', 'CA', '90210',
1, GETUTCDATE(), 'Enterprise', 'America/New_York',
1, GETUTCDATE(), 0
);
SET IDENTITY_INSERT Companies OFF;
PRINT 'Default company inserted';
END
GO
PRINT '=== Step 3: Add CompanyId Columns ===';
-- AspNetUsers
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('AspNetUsers') AND name = 'CompanyId')
BEGIN
ALTER TABLE AspNetUsers ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to AspNetUsers';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('AspNetUsers') AND name = 'CompanyRole')
BEGIN
ALTER TABLE AspNetUsers ADD CompanyRole NVARCHAR(MAX) NULL;
PRINT 'Added CompanyRole to AspNetUsers';
END
-- Other tables
DECLARE @sql NVARCHAR(MAX);
DECLARE @tableName NVARCHAR(128);
DECLARE cur CURSOR FOR
SELECT name FROM sys.tables
WHERE name IN ('Customers', 'Jobs', 'JobItems', 'Quotes', 'QuoteItems',
'InventoryItems', 'InventoryTransactions', 'Equipment',
'MaintenanceRecords', 'Suppliers', 'PricingTiers',
'JobPhotos', 'JobNotes', 'CustomerNotes', 'JobStatusHistory');
OPEN cur;
FETCH NEXT FROM cur INTO @tableName;
WHILE @@FETCH_STATUS = 0
BEGIN
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(@tableName) AND name = 'CompanyId')
BEGIN
SET @sql = N'ALTER TABLE ' + QUOTENAME(@tableName) + N' ADD CompanyId INT NOT NULL DEFAULT 1';
EXEC sp_executesql @sql;
PRINT 'Added CompanyId to ' + @tableName;
END
FETCH NEXT FROM cur INTO @tableName;
END
CLOSE cur;
DEALLOCATE cur;
GO
PRINT '=== Step 4: Create Indexes ===';
DECLARE @indexSql NVARCHAR(MAX);
DECLARE @tbl NVARCHAR(128);
DECLARE @idxName NVARCHAR(256);
DECLARE idxCur CURSOR FOR
SELECT name FROM sys.tables
WHERE name IN ('Customers', 'Jobs', 'Equipment', 'Quotes', 'InventoryItems', 'Suppliers', 'PricingTiers');
OPEN idxCur;
FETCH NEXT FROM idxCur INTO @tbl;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @idxName = 'IX_' + @tbl + '_CompanyId';
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = @idxName)
BEGIN
SET @indexSql = N'CREATE INDEX ' + QUOTENAME(@idxName) + N' ON ' + QUOTENAME(@tbl) + N'(CompanyId)';
EXEC sp_executesql @indexSql;
PRINT 'Created index ' + @idxName;
END
FETCH NEXT FROM idxCur INTO @tbl;
END
CLOSE idxCur;
DEALLOCATE idxCur;
GO
PRINT '=== Step 5: Add Foreign Keys ===';
-- AspNetUsers -> Companies
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = 'FK_AspNetUsers_Companies_CompanyId')
BEGIN
ALTER TABLE AspNetUsers
ADD CONSTRAINT FK_AspNetUsers_Companies_CompanyId
FOREIGN KEY (CompanyId) REFERENCES Companies(Id);
PRINT 'Added FK: AspNetUsers -> Companies';
END
-- All other tables -> Companies
DECLARE @fkSql NVARCHAR(MAX);
DECLARE @table NVARCHAR(128);
DECLARE @fkName NVARCHAR(256);
DECLARE fkCur CURSOR FOR
SELECT name FROM sys.tables
WHERE name IN ('Customers', 'Jobs', 'Equipment', 'Quotes', 'InventoryItems', 'Suppliers', 'PricingTiers');
OPEN fkCur;
FETCH NEXT FROM fkCur INTO @table;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @fkName = 'FK_' + @table + '_Companies_CompanyId';
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE name = @fkName)
BEGIN
SET @fkSql = N'ALTER TABLE ' + QUOTENAME(@table) +
N' ADD CONSTRAINT ' + QUOTENAME(@fkName) +
N' FOREIGN KEY (CompanyId) REFERENCES Companies(Id)';
EXEC sp_executesql @fkSql;
PRINT 'Added FK: ' + @table + ' -> Companies';
END
FETCH NEXT FROM fkCur INTO @table;
END
CLOSE fkCur;
DEALLOCATE fkCur;
GO
PRINT '=== Step 6: Update Migration History ===';
IF NOT EXISTS (SELECT * FROM __EFMigrationsHistory WHERE MigrationId = '20260206004522_AddMultiTenancyFixed')
BEGIN
INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion)
VALUES ('20260206004522_AddMultiTenancyFixed', '8.0.0');
PRINT 'Migration history updated';
END
GO
PRINT '';
PRINT '==============================================';
PRINT 'Multi-Tenancy Migration Completed Successfully!';
PRINT '==============================================';
PRINT 'Next steps:';
PRINT '1. Run the application: dotnet run --project src/PowderCoating.Web';
PRINT '2. Login with: superadmin@powdercoating.com / SuperAdmin123!';
PRINT '==============================================';
GO
-94
View File
@@ -1,94 +0,0 @@
-- ===================================================
-- CLEANUP SEED DATA SCRIPT
-- Use this to remove all seed data and start fresh
-- ===================================================
-- STEP 1: Identify your company ID
SELECT Id, CompanyName, CompanyCode FROM Companies WHERE IsDeleted = 0
-- STEP 2: Set your company ID here
DECLARE @CompanyId INT = 1 -- CHANGE THIS TO YOUR COMPANY ID
PRINT 'Cleaning up seed data for CompanyId: ' + CAST(@CompanyId AS VARCHAR)
-- STEP 3: Delete all seed data in correct order (respecting foreign keys)
BEGIN TRANSACTION
-- Delete Job-related data
PRINT 'Deleting job photos...'
DELETE FROM JobPhotos WHERE JobId IN (SELECT Id FROM Jobs WHERE CompanyId = @CompanyId)
PRINT 'Deleting job notes...'
DELETE FROM JobNotes WHERE JobId IN (SELECT Id FROM Jobs WHERE CompanyId = @CompanyId)
PRINT 'Deleting job items...'
DELETE FROM JobItems WHERE JobId IN (SELECT Id FROM Jobs WHERE CompanyId = @CompanyId)
PRINT 'Deleting jobs...'
DELETE FROM Jobs WHERE CompanyId = @CompanyId
-- Delete Quote-related data
PRINT 'Deleting quote items...'
DELETE FROM QuoteItems WHERE QuoteId IN (SELECT Id FROM Quotes WHERE CompanyId = @CompanyId)
PRINT 'Deleting quotes...'
DELETE FROM Quotes WHERE CompanyId = @CompanyId
-- Delete Maintenance records
PRINT 'Deleting maintenance records...'
DELETE FROM MaintenanceRecords WHERE EquipmentId IN (SELECT Id FROM Equipment WHERE CompanyId = @CompanyId)
-- Delete Equipment
PRINT 'Deleting equipment...'
DELETE FROM Equipment WHERE CompanyId = @CompanyId
-- Delete Inventory-related data
PRINT 'Deleting inventory transactions...'
DELETE FROM InventoryTransactions WHERE InventoryItemId IN (SELECT Id FROM InventoryItems WHERE CompanyId = @CompanyId)
PRINT 'Deleting inventory items...'
DELETE FROM InventoryItems WHERE CompanyId = @CompanyId
-- Delete Catalog
PRINT 'Deleting catalog items...'
DELETE FROM CatalogItems WHERE CompanyId = @CompanyId
-- Delete Customers
PRINT 'Deleting customers...'
DELETE FROM Customers WHERE CompanyId = @CompanyId
-- Delete Pricing Tiers
PRINT 'Deleting pricing tiers...'
DELETE FROM PricingTiers WHERE CompanyId = @CompanyId
-- Delete Operating Costs
PRINT 'Deleting operating costs...'
DELETE FROM CompanyOperatingCosts WHERE CompanyId = @CompanyId
PRINT 'Seed data cleanup complete!'
-- Show what's left
SELECT
'Customers' as TableName, COUNT(*) as RemainingRecords FROM Customers WHERE CompanyId = @CompanyId
UNION ALL
SELECT 'InventoryItems', COUNT(*) FROM InventoryItems WHERE CompanyId = @CompanyId
UNION ALL
SELECT 'Equipment', COUNT(*) FROM Equipment WHERE CompanyId = @CompanyId
UNION ALL
SELECT 'PricingTiers', COUNT(*) FROM PricingTiers WHERE CompanyId = @CompanyId
UNION ALL
SELECT 'Quotes', COUNT(*) FROM Quotes WHERE CompanyId = @CompanyId
UNION ALL
SELECT 'Jobs', COUNT(*) FROM Jobs WHERE CompanyId = @CompanyId
UNION ALL
SELECT 'CatalogItems', COUNT(*) FROM CatalogItems WHERE CompanyId = @CompanyId
COMMIT TRANSACTION
PRINT 'Ready for fresh seed data!'
-- NOTE: This does NOT delete:
-- - The company itself
-- - User accounts
-- - Roles
-- These are preserved so you can log in and re-seed
-63
View File
@@ -1,63 +0,0 @@
-- Complete the multi-tenancy migration
-- Run this to fix the partial migration state
USE PowderCoatingDb;
GO
PRINT 'Checking database state...';
GO
-- Step 1: Ensure default company exists and has correct self-reference
IF EXISTS (SELECT * FROM Companies WHERE Id = 1)
BEGIN
UPDATE Companies SET CompanyId = Id WHERE Id = 1 AND (CompanyId = 0 OR CompanyId IS NULL);
PRINT 'Default company updated';
END
ELSE
BEGIN
-- Insert if doesn't exist
SET IDENTITY_INSERT Companies ON;
INSERT INTO Companies (
Id, CompanyName, CompanyCode, PrimaryContactName, PrimaryContactEmail,
Phone, Address, City, State, ZipCode,
IsActive, SubscriptionStartDate, SubscriptionPlan, TimeZone,
CompanyId, CreatedAt, IsDeleted
) VALUES (
1, 'Demo Company', 'DEMO', 'Admin User', 'admin@demo.com',
'(555) 123-4567', '123 Demo Street', 'Demo City', 'CA', '90210',
1, GETUTCDATE(), 'Enterprise', 'America/New_York',
1, GETUTCDATE(), 0
);
SET IDENTITY_INSERT Companies OFF;
PRINT 'Default company created';
END
GO
-- Step 2: Update all AspNetUsers to reference the default company
UPDATE AspNetUsers
SET CompanyId = 1
WHERE CompanyId = 0 OR CompanyId IS NULL OR CompanyId NOT IN (SELECT Id FROM Companies);
PRINT 'AspNetUsers CompanyId updated';
GO
-- Step 3: Update all other tables to reference default company
UPDATE Customers SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE Jobs SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE JobItems SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE Quotes SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE QuoteItems SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE InventoryItems SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE InventoryTransactions SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE Equipment SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE MaintenanceRecords SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE Suppliers SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE PricingTiers SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE JobPhotos SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE JobNotes SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE CustomerNotes SET CompanyId = 1 WHERE CompanyId = 0;
UPDATE JobStatusHistory SET CompanyId = 1 WHERE CompanyId = 0;
PRINT 'All entity CompanyIds updated to reference default company';
GO
PRINT 'Data migration complete. You can now apply the EF migration.';
GO
-104
View File
@@ -1,104 +0,0 @@
-- Database Diagnostics and Cleanup Script
-- Run this on your testing server to identify and fix seeding issues
-- ===================================================
-- STEP 1: Check for corrupted/malformed SKUs
-- ===================================================
PRINT 'Checking for corrupted inventory items...'
SELECT
Id,
SKU,
Name,
CompanyId,
IsDeleted,
CreatedAt
FROM InventoryItems
WHERE SKU LIKE '(-PWD-%' OR SKU LIKE '%-PWD-%' AND SKU NOT LIKE '[A-Z]%'
ORDER BY CreatedAt DESC
-- ===================================================
-- STEP 2: Check for duplicate SKUs
-- ===================================================
PRINT 'Checking for duplicate SKUs...'
SELECT
SKU,
COUNT(*) as DuplicateCount,
STRING_AGG(CAST(Id AS VARCHAR), ', ') as IDs
FROM InventoryItems
WHERE IsDeleted = 0
GROUP BY SKU
HAVING COUNT(*) > 1
-- ===================================================
-- STEP 3: Check companies and their codes
-- ===================================================
PRINT 'Checking company data...'
SELECT
Id,
CompanyName,
CompanyCode,
IsActive,
IsDeleted,
SubscriptionPlan
FROM Companies
WHERE IsDeleted = 0
ORDER BY Id
-- ===================================================
-- STEP 4: Check inventory count per company
-- ===================================================
PRINT 'Inventory items per company...'
SELECT
c.Id as CompanyId,
c.CompanyName,
c.CompanyCode,
COUNT(i.Id) as InventoryItemCount
FROM Companies c
LEFT JOIN InventoryItems i ON c.Id = i.CompanyId AND i.IsDeleted = 0
WHERE c.IsDeleted = 0
GROUP BY c.Id, c.CompanyName, c.CompanyCode
ORDER BY c.Id
-- ===================================================
-- CLEANUP OPTIONS (commented out for safety)
-- Uncomment the section you need AFTER reviewing the results above
-- ===================================================
-- OPTION A: Delete ALL corrupted/malformed inventory items
/*
DELETE FROM InventoryItems
WHERE SKU LIKE '(-PWD-%' OR (SKU LIKE '%-PWD-%' AND SKU NOT LIKE '[A-Z]%')
PRINT 'Deleted corrupted inventory items'
*/
-- OPTION B: Delete ALL inventory items for a specific company (to re-seed)
/*
DECLARE @CompanyId INT = 1 -- Change this to your company ID
DELETE FROM InventoryItems WHERE CompanyId = @CompanyId
PRINT 'Deleted all inventory items for company ' + CAST(@CompanyId AS VARCHAR)
*/
-- OPTION C: Delete ALL seed data for a complete fresh start (DANGER!)
/*
-- WARNING: This deletes ALL business data but keeps users and companies
DELETE FROM JobPhotos
DELETE FROM JobNotes
DELETE FROM JobItems
DELETE FROM Jobs
DELETE FROM QuoteItems
DELETE FROM Quotes
DELETE FROM InventoryTransactions
DELETE FROM InventoryItems
DELETE FROM Equipment
DELETE FROM MaintenanceRecords
DELETE FROM CatalogItems
DELETE FROM PricingTiers
DELETE FROM CompanyOperatingCosts
DELETE FROM Customers
PRINT 'All seed data deleted - ready for fresh seeding'
*/
-- ===================================================
-- STEP 5: Verify database is ready for seeding
-- ===================================================
PRINT 'Verification complete. Review results above before running cleanup.'
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
-- Remove the NULL row left in __EFMigrationsHistory
DELETE FROM [__EFMigrationsHistory] WHERE [MigrationId] IS NULL;
-- Verify the result
SELECT [MigrationId], [ProductVersion] FROM [__EFMigrationsHistory];
-341
View File
@@ -1,341 +0,0 @@
BEGIN TRANSACTION;
GO
ALTER TABLE [OvenCosts] ADD [DefaultCycleMinutes] int NULL;
GO
ALTER TABLE [OvenCosts] ADD [MaxLoadSqFt] decimal(18,2) NULL;
GO
DECLARE @var0 sysname;
SELECT @var0 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[OvenBatches]') AND [c].[name] = N'EquipmentId');
IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [OvenBatches] DROP CONSTRAINT [' + @var0 + '];');
ALTER TABLE [OvenBatches] ALTER COLUMN [EquipmentId] int NULL;
GO
ALTER TABLE [OvenBatches] ADD [OvenCostId] int NULL;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-14T23:49:48.2095969Z'
WHERE [Id] = 1;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-14T23:49:48.2095976Z'
WHERE [Id] = 2;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-14T23:49:48.2095977Z'
WHERE [Id] = 3;
SELECT @@ROWCOUNT;
GO
CREATE INDEX [IX_OvenBatches_OvenCostId] ON [OvenBatches] ([OvenCostId]);
GO
ALTER TABLE [OvenBatches] ADD CONSTRAINT [FK_OvenBatches_OvenCosts_OvenCostId] FOREIGN KEY ([OvenCostId]) REFERENCES [OvenCosts] ([Id]) ON DELETE NO ACTION;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260314234951_AddOvenCostCapacityFields', N'8.0.11');
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
ALTER TABLE [CompanyPreferences] ADD [PaymentReminderDays] nvarchar(max) NOT NULL DEFAULT N'';
GO
ALTER TABLE [CompanyPreferences] ADD [PaymentRemindersEnabled] bit NOT NULL DEFAULT CAST(0 AS bit);
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T01:01:30.3382335Z'
WHERE [Id] = 1;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T01:01:30.3382342Z'
WHERE [Id] = 2;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T01:01:30.3382345Z'
WHERE [Id] = 3;
SELECT @@ROWCOUNT;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260316010133_AddPaymentReminderPreferences', N'8.0.11');
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
ALTER TABLE [JobItems] DROP CONSTRAINT [FK_JobItems_InventoryItems_PowderInventoryId];
GO
DROP INDEX [IX_JobItems_PowderInventoryId] ON [JobItems];
GO
DECLARE @var1 sysname;
SELECT @var1 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[JobItems]') AND [c].[name] = N'PowderInventoryId');
IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [JobItems] DROP CONSTRAINT [' + @var1 + '];');
ALTER TABLE [JobItems] DROP COLUMN [PowderInventoryId];
GO
DECLARE @var2 sysname;
SELECT @var2 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Equipment]') AND [c].[name] = N'MaxCureTemperatureF');
IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Equipment] DROP CONSTRAINT [' + @var2 + '];');
ALTER TABLE [Equipment] DROP COLUMN [MaxCureTemperatureF];
GO
DECLARE @var3 sysname;
SELECT @var3 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Equipment]') AND [c].[name] = N'MaxLoadWeightLbs');
IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [Equipment] DROP CONSTRAINT [' + @var3 + '];');
ALTER TABLE [Equipment] DROP COLUMN [MaxLoadWeightLbs];
GO
DECLARE @var4 sysname;
SELECT @var4 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Equipment]') AND [c].[name] = N'MinCureTemperatureF');
IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [Equipment] DROP CONSTRAINT [' + @var4 + '];');
ALTER TABLE [Equipment] DROP COLUMN [MinCureTemperatureF];
GO
DECLARE @var5 sysname;
SELECT @var5 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Companies]') AND [c].[name] = N'Settings');
IF @var5 IS NOT NULL EXEC(N'ALTER TABLE [Companies] DROP CONSTRAINT [' + @var5 + '];');
ALTER TABLE [Companies] DROP COLUMN [Settings];
GO
DECLARE @var6 sysname;
SELECT @var6 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[AspNetUsers]') AND [c].[name] = N'DateOfBirth');
IF @var6 IS NOT NULL EXEC(N'ALTER TABLE [AspNetUsers] DROP CONSTRAINT [' + @var6 + '];');
ALTER TABLE [AspNetUsers] DROP COLUMN [DateOfBirth];
GO
DECLARE @var7 sysname;
SELECT @var7 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[AspNetUsers]') AND [c].[name] = N'HourlyRate');
IF @var7 IS NOT NULL EXEC(N'ALTER TABLE [AspNetUsers] DROP CONSTRAINT [' + @var7 + '];');
ALTER TABLE [AspNetUsers] DROP COLUMN [HourlyRate];
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T03:02:22.2139898Z'
WHERE [Id] = 1;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T03:02:22.2139905Z'
WHERE [Id] = 2;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T03:02:22.2139908Z'
WHERE [Id] = 3;
SELECT @@ROWCOUNT;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260316030225_RemoveOrphanedColumns', N'8.0.11');
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T04:02:52.5982681Z'
WHERE [Id] = 1;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T04:02:52.5982692Z'
WHERE [Id] = 2;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T04:02:52.5982693Z'
WHERE [Id] = 3;
SELECT @@ROWCOUNT;
GO
CREATE INDEX [IX_Quotes_CompanyId_IsDeleted] ON [Quotes] ([CompanyId], [IsDeleted]);
GO
CREATE INDEX [IX_Quotes_ExpirationDate] ON [Quotes] ([ExpirationDate]);
GO
CREATE INDEX [IX_Payments_PaymentDate] ON [Payments] ([PaymentDate]);
GO
CREATE INDEX [IX_OvenBatches_ScheduledDate_Status] ON [OvenBatches] ([ScheduledDate], [Status]);
GO
CREATE INDEX [IX_MaintenanceRecords_ScheduledDate] ON [MaintenanceRecords] ([ScheduledDate]);
GO
CREATE INDEX [IX_MaintenanceRecords_Status] ON [MaintenanceRecords] ([Status]);
GO
CREATE INDEX [IX_Jobs_CompanyId_IsDeleted] ON [Jobs] ([CompanyId], [IsDeleted]);
GO
CREATE INDEX [IX_Jobs_DueDate] ON [Jobs] ([DueDate]);
GO
CREATE INDEX [IX_Jobs_ScheduledDate] ON [Jobs] ([ScheduledDate]);
GO
CREATE INDEX [IX_Invoices_CompanyId_IsDeleted] ON [Invoices] ([CompanyId], [IsDeleted]);
GO
CREATE INDEX [IX_Invoices_DueDate] ON [Invoices] ([DueDate]);
GO
CREATE INDEX [IX_Invoices_InvoiceDate] ON [Invoices] ([InvoiceDate]);
GO
CREATE INDEX [IX_Invoices_Status] ON [Invoices] ([Status]);
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260316040255_AddPerformanceIndexesV2', N'8.0.11');
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
ALTER TABLE [CompanyPreferences] ADD [InAccentColor] nvarchar(max) NOT NULL DEFAULT N'';
GO
ALTER TABLE [CompanyPreferences] ADD [InDefaultTerms] nvarchar(max) NULL;
GO
ALTER TABLE [CompanyPreferences] ADD [InFooterNote] nvarchar(max) NULL;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T12:42:13.7414482Z'
WHERE [Id] = 1;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T12:42:13.7414534Z'
WHERE [Id] = 2;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T12:42:13.7414536Z'
WHERE [Id] = 3;
SELECT @@ROWCOUNT;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260316124217_AddInvoicePdfTemplate', N'8.0.11');
GO
COMMIT;
GO
BEGIN TRANSACTION;
GO
ALTER TABLE [BugReports] ADD [CompanyName] nvarchar(max) NULL;
GO
CREATE TABLE [BugReportAttachments] (
[Id] int NOT NULL IDENTITY,
[BugReportId] int NOT NULL,
[BlobPath] nvarchar(max) NOT NULL,
[FileName] nvarchar(max) NOT NULL,
[ContentType] nvarchar(max) NOT NULL,
[FileSizeBytes] bigint NOT NULL,
[CompanyId] int NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_BugReportAttachments] PRIMARY KEY ([Id]),
CONSTRAINT [FK_BugReportAttachments_BugReports_BugReportId] FOREIGN KEY ([BugReportId]) REFERENCES [BugReports] ([Id]) ON DELETE CASCADE
);
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T14:27:16.8052839Z'
WHERE [Id] = 1;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T14:27:16.8052845Z'
WHERE [Id] = 2;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CreatedAt] = '2026-03-16T14:27:16.8052846Z'
WHERE [Id] = 3;
SELECT @@ROWCOUNT;
GO
CREATE INDEX [IX_BugReportAttachments_BugReportId] ON [BugReportAttachments] ([BugReportId]);
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260316142720_AddBugReportAttachments', N'8.0.11');
GO
COMMIT;
GO
-2
View File
@@ -1,2 +0,0 @@
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES ('20260223000001_AddEmailSenderToPreferences', '8.0.11');
-10
View File
@@ -1,10 +0,0 @@
-- ============================================================
-- EF Migrations Rebase Script - Remote Dev Server
-- Replaces all old migration history entries with single Baseline
-- NO schema changes are made - data is untouched
-- ============================================================
DELETE FROM [__EFMigrationsHistory];
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES ('20260316155002_Baseline', '8.0.11');
-74
View File
@@ -1,74 +0,0 @@
-- Compare admin@demo.com vs admin@powdercoating.com to find differences
-- Run this against PowderCoatingDb
-- Check both users side by side
SELECT
'admin@demo.com' as UserAccount,
Id,
UserName,
Email,
EmailConfirmed,
PhoneNumber,
PhoneNumberConfirmed,
TwoFactorEnabled,
LockoutEnd,
LockoutEnabled,
AccessFailedCount,
CompanyId,
CompanyRole,
FirstName,
LastName,
LEN(SecurityStamp) as SecurityStampLength,
LEN(ConcurrencyStamp) as ConcurrencyStampLength,
LEN(PasswordHash) as PasswordHashLength,
LEN(NormalizedUserName) as NormalizedUserNameLength,
LEN(NormalizedEmail) as NormalizedEmailLength
FROM AspNetUsers
WHERE Email = 'admin@demo.com'
UNION ALL
SELECT
'admin@powdercoating.com' as UserAccount,
Id,
UserName,
Email,
EmailConfirmed,
PhoneNumber,
PhoneNumberConfirmed,
TwoFactorEnabled,
LockoutEnd,
LockoutEnabled,
AccessFailedCount,
CompanyId,
CompanyRole,
FirstName,
LastName,
LEN(SecurityStamp) as SecurityStampLength,
LEN(ConcurrencyStamp) as ConcurrencyStampLength,
LEN(PasswordHash) as PasswordHashLength,
LEN(NormalizedUserName) as NormalizedUserNameLength,
LEN(NormalizedEmail) as NormalizedEmailLength
FROM AspNetUsers
WHERE Email = 'admin@powdercoating.com';
-- Check for any user claims
SELECT
u.Email,
uc.ClaimType,
uc.ClaimValue,
LEN(uc.ClaimValue) as ClaimValueLength
FROM AspNetUserClaims uc
INNER JOIN AspNetUsers u ON u.Id = uc.UserId
WHERE u.Email IN ('admin@demo.com', 'admin@powdercoating.com')
ORDER BY u.Email;
-- Check user roles
SELECT
u.Email,
r.Name as RoleName
FROM AspNetUsers u
INNER JOIN AspNetUserRoles ur ON u.Id = ur.UserId
INNER JOIN AspNetRoles r ON ur.RoleId = r.Id
WHERE u.Email IN ('admin@demo.com', 'admin@powdercoating.com')
ORDER BY u.Email;
-32
View File
@@ -1,32 +0,0 @@
I looked through the reporting code, and I do not see a dedicated sales tax report.
What exists today:
- The report menu has Financial Summary, AR Aging, Balance Sheet, and Sales & Income, but no sales-tax-specific report card in /Y:/PCC/
PowderCoatingApp/src/PowderCoating.Web/Views/Reports/Landing.cshtml:147 and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Web/Views/Reports/
Landing.cshtml:211.
- The reporting service only exposes four finance reports in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Application/Interfaces/
IFinancialReportService.cs:13 through /Y:/PCC/PowderCoatingApp/src/PowderCoating.Application/Interfaces/IFinancialReportService.cs:22.
- Sales & Income does surface tax, but only as a total and per-invoice column in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Infrastructure/
Services/FinancialReportService.cs:406, /Y:/PCC/PowderCoatingApp/src/PowderCoating.Infrastructure/Services/FinancialReportService.cs:419,
and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Web/Views/Reports/SalesAndIncome.cshtml:268.
- The good news is the underlying data is already there: invoices store TaxAmount and SalesTaxAccountId in /Y:/PCC/PowderCoatingApp/src/
PowderCoating.Core/Entities/Invoice.cs:23 and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Core/Entities/Invoice.cs:55, and the chart of
accounts seeds 2200 Sales Tax Payable in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Infrastructure/Services/SeedDataService.Accounts.cs:59.
Plan Id recommend:
1. Add a new Sales Tax report under Reports > Finance with date range filters, matching the existing report pattern in /Y:/PCC/PowderCoatingApp/
src/PowderCoating.Web/Controllers/ReportsController.cs:993.
2. Build a SalesTaxReportDto plus GetSalesTaxReportAsync(...) in the reporting interface/service.
3. Phase 1 report contents:
- Total taxable sales
- Total non-taxed sales
- Total sales tax billed
- Breakdown by sales-tax liability account
- Breakdown by month
- Invoice detail grid: invoice date, invoice #, customer, subtotal, tax %, tax amount, total, amount paid, balance due, status, tax account
4. Add PDF export, and probably CSV too, since this is the kind of report people hand to accountants.
5. Put a report card on the Reports landing page and gate it the same way as the other accounting reports.
View File
-62
View File
@@ -1,62 +0,0 @@
-- Find what's different between working and non-working users
-- Run against PowderCoatingDb
-- Compare all three users
SELECT
Email,
UserName,
-- Check for null or empty critical fields
CASE WHEN PasswordHash IS NULL THEN 'NULL'
WHEN LEN(PasswordHash) = 0 THEN 'EMPTY'
ELSE CAST(LEN(PasswordHash) AS VARCHAR) END as PasswordHashStatus,
CASE WHEN SecurityStamp IS NULL THEN 'NULL'
WHEN LEN(SecurityStamp) = 0 THEN 'EMPTY'
ELSE CAST(LEN(SecurityStamp) AS VARCHAR) END as SecurityStampStatus,
CASE WHEN ConcurrencyStamp IS NULL THEN 'NULL'
WHEN LEN(ConcurrencyStamp) = 0 THEN 'EMPTY'
ELSE CAST(LEN(ConcurrencyStamp) AS VARCHAR) END as ConcurrencyStampStatus,
NormalizedUserName,
NormalizedEmail,
CompanyRole,
CompanyId
FROM AspNetUsers
WHERE Email IN ('admin@powdercoating.com', 'admin@demo.com')
OR UserName = 'superadmin'
ORDER BY Email;
-- Check for any stored claims that might be corrupted
SELECT
u.Email,
uc.ClaimType,
uc.ClaimValue,
LEN(uc.ClaimValue) as ValueLength,
-- Check if value contains special characters
CASE
WHEN uc.ClaimValue LIKE '%[^a-zA-Z0-9@._-]%' THEN 'Contains special chars'
ELSE 'OK'
END as ValidationStatus
FROM AspNetUserClaims uc
INNER JOIN AspNetUsers u ON u.Id = uc.UserId
WHERE u.Email IN ('admin@powdercoating.com', 'admin@demo.com')
OR u.UserName = 'superadmin';
-- Check user tokens (these can also cause issues)
SELECT
u.Email,
ut.LoginProvider,
ut.Name,
LEN(ut.Value) as ValueLength
FROM AspNetUserTokens ut
INNER JOIN AspNetUsers u ON u.Id = ut.UserId
WHERE u.Email IN ('admin@powdercoating.com', 'admin@demo.com')
OR u.UserName = 'superadmin';
-- Check for any unusual characters in key fields
SELECT
Email,
CASE WHEN Email LIKE '%[^a-zA-Z0-9@._-]%' THEN 'SUSPICIOUS' ELSE 'OK' END as EmailCheck,
CASE WHEN UserName LIKE '%[^a-zA-Z0-9@._-]%' THEN 'SUSPICIOUS' ELSE 'OK' END as UserNameCheck,
CASE WHEN CompanyRole LIKE '%[^a-zA-Z0-9]%' THEN 'SUSPICIOUS' ELSE 'OK' END as CompanyRoleCheck
FROM AspNetUsers
WHERE Email IN ('admin@powdercoating.com', 'admin@demo.com')
OR UserName = 'superadmin';
-26
View File
@@ -1,26 +0,0 @@
-- Fix admin@demo.com user by resetting security stamp and clearing any corrupted claims
-- Run this against the PowderCoatingDb database
-- Update security stamp (forces re-authentication)
UPDATE AspNetUsers
SET SecurityStamp = NEWID(),
ConcurrencyStamp = NEWID()
WHERE Email = 'admin@demo.com';
-- Delete any potentially corrupted user claims
DELETE FROM AspNetUserClaims
WHERE UserId = (SELECT Id FROM AspNetUsers WHERE Email = 'admin@demo.com');
-- Verify the user record
SELECT
Id,
UserName,
Email,
CompanyId,
CompanyRole,
SecurityStamp,
ConcurrencyStamp
FROM AspNetUsers
WHERE Email = 'admin@demo.com';
PRINT 'admin@demo.com user has been reset. Please log out and log back in.';
-30
View File
@@ -1,30 +0,0 @@
-- Fix the CompanyRole field for admin@demo.com
-- Run this against PowderCoatingDb
-- First, let's see the current value
SELECT
Email,
CompanyRole,
LEN(CompanyRole) as RoleLength,
CAST(CompanyRole AS VARBINARY(MAX)) as RoleBytes
FROM AspNetUsers
WHERE Email = 'admin@demo.com';
-- Update to a clean value
UPDATE AspNetUsers
SET CompanyRole = 'CompanyAdmin',
SecurityStamp = NEWID(),
ConcurrencyStamp = NEWID()
WHERE Email = 'admin@demo.com';
-- Verify the fix
SELECT
Email,
CompanyRole,
CompanyId,
FirstName,
LastName
FROM AspNetUsers
WHERE Email = 'admin@demo.com';
PRINT 'CompanyRole has been reset to CompanyAdmin.';
-61
View File
@@ -1,61 +0,0 @@
-- ============================================================================
-- Fix Customer Email Index for Multi-Tenancy
-- This allows the same email to exist across different companies
-- but prevents duplicate emails within the same company
-- ============================================================================
USE PowderCoatingDb
GO
-- Step 1: Check if the old index exists
IF EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Customers_Email' AND object_id = OBJECT_ID('Customers'))
BEGIN
PRINT 'Dropping old IX_Customers_Email index...'
-- Drop the old unique index that enforces global email uniqueness
DROP INDEX IX_Customers_Email ON Customers
PRINT 'Old index dropped successfully.'
END
ELSE
BEGIN
PRINT 'Old index IX_Customers_Email not found (may have already been dropped).'
END
GO
-- Step 2: Create new unique index scoped to CompanyId
PRINT 'Creating new company-scoped IX_Customers_Email index...'
CREATE UNIQUE INDEX IX_Customers_Email
ON Customers (CompanyId, Email)
WHERE [Email] IS NOT NULL AND [IsDeleted] = 0
GO
PRINT 'New index created successfully!'
PRINT ''
PRINT 'The Customers table now allows:'
PRINT ' ✓ Same email across different companies'
PRINT ' ✓ Prevents duplicate emails within the same company'
PRINT ' ✓ Ignores soft-deleted records'
GO
-- Step 3: Verify the new index
SELECT
i.name AS IndexName,
i.is_unique AS IsUnique,
STUFF((
SELECT ', ' + COL_NAME(ic.object_id, ic.column_id)
FROM sys.index_columns ic
WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id
ORDER BY ic.key_ordinal
FOR XML PATH('')
), 1, 2, '') AS IndexColumns,
i.filter_definition AS FilterDefinition
FROM sys.indexes i
WHERE i.object_id = OBJECT_ID('Customers')
AND i.name = 'IX_Customers_Email'
GO
PRINT ''
PRINT 'Index verification complete. You can now seed customer data!'
GO
-74
View File
@@ -1,74 +0,0 @@
-- =============================================================================
-- fix-inventory-categories.sql
-- Links inventory items that have a Category string but no InventoryCategoryId.
-- This affects items imported via CSV before the category fix was applied.
-- Safe to run multiple times (only touches items with InventoryCategoryId IS NULL).
-- Run against: PowderCoatingDb
-- =============================================================================
-- STEP 1: Preview what will be updated (run this first, review before proceeding)
SELECT
ii.Id,
ii.SKU,
ii.Name,
ii.CompanyId,
ii.Category AS CurrentCategory,
cat.DisplayName AS ResolvedTo,
cat.CategoryCode,
cat.IsCoating
FROM InventoryItems ii
JOIN InventoryCategoryLookups cat
ON cat.CompanyId = ii.CompanyId
AND cat.IsDeleted = 0
AND ii.IsDeleted = 0
AND ii.InventoryCategoryId IS NULL
AND (
ii.Category = cat.DisplayName
OR ii.Category = cat.CategoryCode
OR (cat.CategoryCode = 'POWDER' AND ii.Category IN ('Powder Coatings','Powder Coating','Powders','Powder'))
OR (cat.CategoryCode = 'PRIMER' AND ii.Category IN ('Primers','Primer'))
OR (cat.CategoryCode = 'CLEANER' AND ii.Category IN ('Cleaners','Cleaner'))
OR (cat.CategoryCode = 'MASKING' AND ii.Category IN ('Masking','Masking Tape','Masking Supplies'))
OR (cat.CategoryCode = 'ABRASIVE' AND ii.Category IN ('Abrasive','Abrasives','Blast Media','Abrasive Media'))
OR (cat.CategoryCode = 'CHEMICAL' AND ii.Category IN ('Chemicals','Chemical'))
OR (cat.CategoryCode = 'CONSUMABLE' AND ii.Category IN ('Consumable','Consumables'))
OR (cat.CategoryCode = 'TOOL' AND ii.Category IN ('Tools','Tool','Tools & Equipment','Equipment'))
OR (cat.CategoryCode = 'OTHER' AND ii.Category IN ('General','Other'))
)
ORDER BY ii.CompanyId, ii.Id
-- STEP 2: Apply the fix (run after reviewing Step 1)
UPDATE ii
SET
ii.InventoryCategoryId = cat.Id,
ii.Category = cat.DisplayName,
ii.UpdatedAt = GETDATE()
FROM InventoryItems ii
JOIN InventoryCategoryLookups cat
ON cat.CompanyId = ii.CompanyId
AND cat.IsDeleted = 0
AND ii.IsDeleted = 0
AND ii.InventoryCategoryId IS NULL
AND (
ii.Category = cat.DisplayName
OR ii.Category = cat.CategoryCode
OR (cat.CategoryCode = 'POWDER' AND ii.Category IN ('Powder Coatings','Powder Coating','Powders','Powder'))
OR (cat.CategoryCode = 'PRIMER' AND ii.Category IN ('Primers','Primer'))
OR (cat.CategoryCode = 'CLEANER' AND ii.Category IN ('Cleaners','Cleaner'))
OR (cat.CategoryCode = 'MASKING' AND ii.Category IN ('Masking','Masking Tape','Masking Supplies'))
OR (cat.CategoryCode = 'ABRASIVE' AND ii.Category IN ('Abrasive','Abrasives','Blast Media','Abrasive Media'))
OR (cat.CategoryCode = 'CHEMICAL' AND ii.Category IN ('Chemicals','Chemical'))
OR (cat.CategoryCode = 'CONSUMABLE' AND ii.Category IN ('Consumable','Consumables'))
OR (cat.CategoryCode = 'TOOL' AND ii.Category IN ('Tools','Tool','Tools & Equipment','Equipment'))
OR (cat.CategoryCode = 'OTHER' AND ii.Category IN ('General','Other'))
)
-- STEP 3: Check for any remaining unmatched items (need manual review in the UI)
SELECT Id, SKU, Name, CompanyId, Category
FROM InventoryItems
WHERE IsDeleted = 0
AND InventoryCategoryId IS NULL
AND Category IS NOT NULL
ORDER BY CompanyId, Category
-168
View File
@@ -1,168 +0,0 @@
-- Pre-migration script to prepare database for multi-tenancy
-- Run this BEFORE applying the AddMultiTenancy migration
USE PowderCoatingDb;
GO
-- Step 1: Create Companies table manually (without constraints initially)
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Companies')
BEGIN
CREATE TABLE Companies (
Id INT IDENTITY(1,1) PRIMARY KEY,
CompanyName NVARCHAR(MAX) NOT NULL,
CompanyCode NVARCHAR(450) NULL,
PrimaryContactName NVARCHAR(MAX) NOT NULL,
PrimaryContactEmail NVARCHAR(MAX) NOT NULL,
Phone NVARCHAR(MAX) NULL,
Address NVARCHAR(MAX) NULL,
City NVARCHAR(MAX) NULL,
State NVARCHAR(MAX) NULL,
ZipCode NVARCHAR(MAX) NULL,
IsActive BIT NOT NULL DEFAULT 1,
SubscriptionStartDate DATETIME2 NOT NULL,
SubscriptionEndDate DATETIME2 NULL,
SubscriptionPlan NVARCHAR(MAX) NULL,
TimeZone NVARCHAR(MAX) NULL,
LogoPath NVARCHAR(MAX) NULL,
Settings NVARCHAR(MAX) NULL,
CompanyId INT NOT NULL DEFAULT 0,
CreatedAt DATETIME2 NOT NULL,
UpdatedAt DATETIME2 NULL,
CreatedBy NVARCHAR(MAX) NULL,
UpdatedBy NVARCHAR(MAX) NULL,
IsDeleted BIT NOT NULL DEFAULT 0,
DeletedAt DATETIME2 NULL,
DeletedBy NVARCHAR(MAX) NULL
);
-- Insert default company
INSERT INTO Companies (
CompanyName, CompanyCode, PrimaryContactName, PrimaryContactEmail,
Phone, Address, City, State, ZipCode,
IsActive, SubscriptionStartDate, SubscriptionPlan, TimeZone,
CompanyId, CreatedAt, IsDeleted
) VALUES (
'Demo Company', 'DEMO', 'Admin User', 'admin@demo.com',
'(555) 123-4567', '123 Demo Street', 'Demo City', 'CA', '90210',
1, GETUTCDATE(), 'Enterprise', 'America/New_York',
0, GETUTCDATE(), 0
);
-- Update CompanyId to self-reference
UPDATE Companies SET CompanyId = Id WHERE Id = 1;
PRINT 'Default company created with ID = 1';
END
ELSE
BEGIN
PRINT 'Companies table already exists';
END
GO
-- Step 2: Add CompanyId columns to all tables (if they don't exist)
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('AspNetUsers') AND name = 'CompanyId')
BEGIN
ALTER TABLE AspNetUsers ADD CompanyId INT NULL;
UPDATE AspNetUsers SET CompanyId = 1 WHERE CompanyId IS NULL;
ALTER TABLE AspNetUsers ALTER COLUMN CompanyId INT NOT NULL;
PRINT 'Added CompanyId to AspNetUsers';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('Customers') AND name = 'CompanyId')
BEGIN
ALTER TABLE Customers ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to Customers';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('Jobs') AND name = 'CompanyId')
BEGIN
ALTER TABLE Jobs ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to Jobs';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('JobItems') AND name = 'CompanyId')
BEGIN
ALTER TABLE JobItems ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to JobItems';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('Quotes') AND name = 'CompanyId')
BEGIN
ALTER TABLE Quotes ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to Quotes';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('QuoteItems') AND name = 'CompanyId')
BEGIN
ALTER TABLE QuoteItems ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to QuoteItems';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('InventoryItems') AND name = 'CompanyId')
BEGIN
ALTER TABLE InventoryItems ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to InventoryItems';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('InventoryTransactions') AND name = 'CompanyId')
BEGIN
ALTER TABLE InventoryTransactions ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to InventoryTransactions';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('Equipment') AND name = 'CompanyId')
BEGIN
ALTER TABLE Equipment ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to Equipment';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('MaintenanceRecords') AND name = 'CompanyId')
BEGIN
ALTER TABLE MaintenanceRecords ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to MaintenanceRecords';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('Suppliers') AND name = 'CompanyId')
BEGIN
ALTER TABLE Suppliers ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to Suppliers';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('PricingTiers') AND name = 'CompanyId')
BEGIN
ALTER TABLE PricingTiers ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to PricingTiers';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('JobPhotos') AND name = 'CompanyId')
BEGIN
ALTER TABLE JobPhotos ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to JobPhotos';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('JobNotes') AND name = 'CompanyId')
BEGIN
ALTER TABLE JobNotes ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to JobNotes';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('CustomerNotes') AND name = 'CompanyId')
BEGIN
ALTER TABLE CustomerNotes ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to CustomerNotes';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('JobStatusHistory') AND name = 'CompanyId')
BEGIN
ALTER TABLE JobStatusHistory ADD CompanyId INT NOT NULL DEFAULT 1;
PRINT 'Added CompanyId to JobStatusHistory';
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('AspNetUsers') AND name = 'CompanyRole')
BEGIN
ALTER TABLE AspNetUsers ADD CompanyRole NVARCHAR(MAX) NULL;
PRINT 'Added CompanyRole to AspNetUsers';
END
PRINT 'Database prepared for multi-tenancy migration';
GO
-61
View File
@@ -1,61 +0,0 @@
-- Run this AFTER applying the schema script from the old database.
-- Marks all 43 EF migrations as applied so EF Core won't try to re-run them.
IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
BEGIN
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);
END;
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
SELECT MigrationId, ProductVersion FROM (VALUES
('20260316155002_Baseline', '8.0.11'),
('20260317121938_AddAiContextProfile', '8.0.11'),
('20260317205927_FixLaborItemQuantityDecimal', '8.0.11'),
('20260318124847_AddJobTimeEntries', '8.0.11'),
('20260318131500_AddJobShopAccessCode', '8.0.11'),
('20260318132857_AddShopWorkerRoleCosts', '8.0.11'),
('20260318134236_AddReworkTracking', '8.0.11'),
('20260318222648_AddRefundsAndCreditMemos', '8.0.11'),
('20260319023827_AddJobTemplates', '8.0.11'),
('20260319154506_AddGiftCertificates', '8.0.11'),
('20260320002450_AddRefundStoreCreditLink', '8.0.11'),
('20260320005106_AddQuoteItemIsAiItem', '8.0.11'),
('20260320011057_AddQuotePricingSnapshot', '8.0.11'),
('20260320231509_AddStripeConnectAndOnlinePayments', '8.0.11'),
('20260326230438_AddQuotePhotoSubscriptionLimits', '8.0.11'),
('20260328133627_AddJobPhotoIsAiAnalysisPhoto', '8.0.11'),
('20260329003300_AddJobDiscountRushFields', '8.0.11'),
('20260329005838_AddDeposits', '8.0.11'),
('20260329134753_AddMerchandise', '8.0.11'),
('20260329141137_AddGiftCertificateInvoiceItems', '8.0.11'),
('20260330234034_AddSalesItemFields', '8.0.11'),
('20260401125630_AddQuoteDepositPaymentFields', '8.0.11'),
('20260401131724_AddUniqueDocumentNumberConstraints', '8.0.11'),
('20260401141653_FixGiftCertificateUniqueIndexPerCompany', '8.0.11'),
('20260402015422_AddInvoiceExternalReference', '8.0.11'),
('20260402032156_AddMigratingFromQuickBooks', '8.0.11'),
('20260402165758_AddQbMigrationStateJson', '8.0.11'),
('20260402184721_FixInventorySkuUniqueIndex', '8.0.11'),
('20260402185216_FixJobShopAccessCodeUniqueIndex', '8.0.11'),
('20260402224949_AddDashboardTips', '8.0.11'),
('20260403000650_AddStripeWebhookEvents', '8.0.11'),
('20260404151636_AddAllowAccountingToPlan', '8.0.11'),
('20260404194126_AddBillReceiptFilePath', '8.0.11'),
('20260405003350_AddPerformanceIndexes', '8.0.11'),
('20260405155653_AddPlatformSettings', '8.0.11'),
('20260405161241_AddPlatformSettingsV2', '8.0.11'),
('20260405162137_UpdateAdminEmailDescription', '8.0.11'),
('20260406191501_MakeBillLineItemAccountIdNullable', '8.0.11'),
('20260408205345_AddJobIntakeFields', '8.0.11'),
('20260409013822_AddInAppNotifications', '8.0.11'),
('20260410021934_AddLegalCompliance', '8.0.11'),
('20260410025353_AddAiFeaturesToPlanConfig', '8.0.11'),
('20260410032027_AddTrialsEnabledSetting', '8.0.11')
) AS v(MigrationId, ProductVersion)
WHERE NOT EXISTS (
SELECT 1 FROM [__EFMigrationsHistory] WHERE [MigrationId] = v.MigrationId
);
-160
View File
@@ -1,160 +0,0 @@
BEGIN TRANSACTION;
GO
ALTER TABLE [Suppliers] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [Quotes] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [QuoteItems] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [PricingTiers] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [MaintenanceRecords] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [JobStatusHistory] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [Jobs] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [JobPhotos] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [JobNotes] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [JobItems] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [InventoryTransactions] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [InventoryItems] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [Equipment] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [Customers] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [CustomerNotes] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [AspNetUsers] ADD [CompanyId] int NOT NULL DEFAULT 0;
GO
ALTER TABLE [AspNetUsers] ADD [CompanyRole] nvarchar(max) NULL;
GO
CREATE TABLE [Companies] (
[Id] int NOT NULL IDENTITY,
[CompanyName] nvarchar(max) NOT NULL,
[CompanyCode] nvarchar(450) NULL,
[PrimaryContactName] nvarchar(max) NOT NULL,
[PrimaryContactEmail] nvarchar(max) NOT NULL,
[Phone] nvarchar(max) NULL,
[Address] nvarchar(max) NULL,
[City] nvarchar(max) NULL,
[State] nvarchar(max) NULL,
[ZipCode] nvarchar(max) NULL,
[IsActive] bit NOT NULL,
[SubscriptionStartDate] datetime2 NOT NULL,
[SubscriptionEndDate] datetime2 NULL,
[SubscriptionPlan] nvarchar(max) NULL,
[TimeZone] nvarchar(max) NULL,
[LogoPath] nvarchar(max) NULL,
[Settings] nvarchar(max) NULL,
[CompanyId] int NOT NULL,
[CreatedAt] datetime2 NOT NULL,
[UpdatedAt] datetime2 NULL,
[CreatedBy] nvarchar(max) NULL,
[UpdatedBy] nvarchar(max) NULL,
[IsDeleted] bit NOT NULL,
[DeletedAt] datetime2 NULL,
[DeletedBy] nvarchar(max) NULL,
CONSTRAINT [PK_Companies] PRIMARY KEY ([Id])
);
GO
UPDATE [PricingTiers] SET [CompanyId] = 0, [CreatedAt] = '2026-02-06T00:44:39.1275198Z'
WHERE [Id] = 1;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CompanyId] = 0, [CreatedAt] = '2026-02-06T00:44:39.1275205Z'
WHERE [Id] = 2;
SELECT @@ROWCOUNT;
GO
UPDATE [PricingTiers] SET [CompanyId] = 0, [CreatedAt] = '2026-02-06T00:44:39.1275207Z'
WHERE [Id] = 3;
SELECT @@ROWCOUNT;
GO
CREATE INDEX [IX_Suppliers_CompanyId] ON [Suppliers] ([CompanyId]);
GO
CREATE INDEX [IX_Quotes_CompanyId] ON [Quotes] ([CompanyId]);
GO
CREATE INDEX [IX_PricingTiers_CompanyId] ON [PricingTiers] ([CompanyId]);
GO
CREATE INDEX [IX_Jobs_CompanyId] ON [Jobs] ([CompanyId]);
GO
CREATE INDEX [IX_InventoryItems_CompanyId] ON [InventoryItems] ([CompanyId]);
GO
CREATE INDEX [IX_Equipment_CompanyId] ON [Equipment] ([CompanyId]);
GO
CREATE INDEX [IX_Customers_CompanyId] ON [Customers] ([CompanyId]);
GO
CREATE INDEX [IX_AspNetUsers_CompanyId] ON [AspNetUsers] ([CompanyId]);
GO
CREATE UNIQUE INDEX [IX_Companies_CompanyCode] ON [Companies] ([CompanyCode]) WHERE [CompanyCode] IS NOT NULL;
GO
ALTER TABLE [AspNetUsers] ADD CONSTRAINT [FK_AspNetUsers_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) ON DELETE NO ACTION;
GO
ALTER TABLE [Customers] ADD CONSTRAINT [FK_Customers_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) ON DELETE NO ACTION;
GO
ALTER TABLE [Equipment] ADD CONSTRAINT [FK_Equipment_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) ON DELETE NO ACTION;
GO
ALTER TABLE [InventoryItems] ADD CONSTRAINT [FK_InventoryItems_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) ON DELETE NO ACTION;
GO
ALTER TABLE [Jobs] ADD CONSTRAINT [FK_Jobs_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) ON DELETE NO ACTION;
GO
ALTER TABLE [PricingTiers] ADD CONSTRAINT [FK_PricingTiers_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) ON DELETE NO ACTION;
GO
ALTER TABLE [Quotes] ADD CONSTRAINT [FK_Quotes_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) ON DELETE NO ACTION;
GO
ALTER TABLE [Suppliers] ADD CONSTRAINT [FK_Suppliers_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [Companies] ([Id]) ON DELETE NO ACTION;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260206004522_AddMultiTenancyFixed', N'8.0.11');
GO
COMMIT;
GO
File diff suppressed because it is too large Load Diff
-4095
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-2613
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More