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>
11 KiB
CLAUDE.md
Guidance for Claude Code when working in this repository.
Project Overview
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
# Build
dotnet build
# Web MVC — https://localhost:58461
cd src/PowderCoating.Web && dotnet run
# API — Swagger at root URL
cd src/PowderCoating.Api && dotnet run
# Tests
dotnet test
dotnet test tests/PowderCoating.UnitTests
dotnet test tests/PowderCoating.IntegrationTests
Database (EF Core)
Run from src/PowderCoating.Web. Always include --context ApplicationDbContext — multiple DbContexts exist; omitting it throws.
cd src/PowderCoating.Web
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
SuperAdmin (break glass): artemis@powdercoatinglogix.com / SuperAdmin123!
SuperAdmin (seed): superadmin@powdercoatinglogix.com / SuperAdmin123!
SuperAdmin (seed): spouliot@powdercoatinglogix.com / SuperAdmin123!
Company Admin (seed): demo@powdercoatinglogix.com / CompanyAdmin123!
Architecture
Layers
- Core — Entities, enums, repository + service interfaces.
BaseEntityprovidesId,CompanyId,CreatedAt,UpdatedAt,IsDeleted, audit fields on every entity. - Application — DTOs, AutoMapper profiles (auto-discovered via
cfg.AddMaps();PricingTierProfileis an exception — registered manually inProgram.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.
Global Query Filters (always active)
- Soft deletes:
IsDeleted == false - Multi-tenancy: non-SuperAdmin sees only their
CompanyId - Bypass:
ignoreQueryFilters: trueon repository methods
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)
ApplicationDbContextis NEVER injected into a controller. All data access goes throughIUnitOfWork. Enforced at startup byEnforceDataAccessArchitecture()inProgram.cs. Full rationale + permanent exceptions:docs/DATA_ACCESS_ARCHITECTURE.md
Three tiers — use the right one:
Tier 1 — Simple CRUD → IUnitOfWork.EntityName (generic IRepository<T>)
var items = await _unitOfWork.CatalogItems.GetAllAsync();
await _unitOfWork.Announcements.AddAsync(entity);
await _unitOfWork.CompleteAsync();
Tier 2 — Complex domain queries → typed repositories on IUnitOfWork
var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id);
var invoice = await _unitOfWork.Invoices.LoadForViewAsync(id);
var quote = await _unitOfWork.Quotes.GetByApprovalTokenAsync(token);
Typed repos: IJobRepository, IInvoiceRepository, IQuoteRepository, ICustomerRepository, IBillRepository, IPurchaseOrderRepository
— defined in Core/Interfaces/Repositories/, implemented in Infrastructure/Repositories/.
Tier 3 — Aggregate/reporting → injected read services
var aging = await _financialReports.GetArAgingAsync(companyId);
Services: IFinancialReportService, IOperationalReportService.
Permanent exceptions (ApplicationDbContext allowed — intentional):
StripeWebhookController, WebhooksController, PaymentController, RegistrationController,
DataExportController, AccountDataExportController, DataPurgeController,
SystemInfoController, SystemLogsController, CompanyHealthController
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.
Pricing
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):TaxPercentdefaults to 0 on quote/invoice create; marked ★ in dropdowns
Pricing Routing Flags — Must Stay In Sync Across All Three Layers
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 |
|---|---|
IsAiItem |
Job repriced as calculated item; oven cost double-charged on every save |
IsGenericItem |
ManualUnitPrice ignored; price recalculated from surface area |
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 flag:
- Add to
QuoteItem(Core/Entities) - Add to
JobItem(Core/Entities) - Add to
CreateQuoteItemDto(Application/DTOs) - Add to
JobItemSeed(private class inJobItemAssemblyService) - Map in all three
JobItemAssemblyService.CreateJobItemoverloads - Include in every
existingItemsDataJSON block inEdit.cshtml,EditItems.cshtml, and all controller actions that buildCreateQuoteItemDtofrom aJobItem - Add migration if field is new on a persisted entity
- Structural test
PricingRoutingFlags_ExistOnBothQuoteItemAndJobItemfails until steps 1–3 are done — intentional
Configuration
Key Settings (src/PowderCoating.Web/appsettings.json)
- DB:
ConnectionStrings:DefaultConnection(SQL Server Express) - AI:
AI:Anthropic:ApiKey— Anthropic Claudeclaude-sonnet-4-6, NOT OpenAI - Ports: HTTPS 58461 / HTTP 58462
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— NOTRequireAdministratorRole(that policy is unregistered and will throw)
File Uploads
Limits in AppConstants.cs: 10 MB max, allowed: jpg/jpeg/png/gif/pdf/doc/docx/xls/xlsx.
UI Rules
- HTML entities in
.cshtml——not—,×not×,…not…. Literal Unicode gets corrupted by AI tools + Windows file encoding. - External JS files only — put scripts in
wwwroot/js/*.js, reference viasrc=. Inline@section Scriptsblocks can silently fail with SyntaxErrors from layout HTML context. alert-permanentCSS class —_Layoutauto-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).
Gotchas
- Two data export controllers:
DataExportController(SuperAdmin) andAccountDataExportController(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 inViews/Help/(human-readable help center). - Demo reset:
DemoController.ResetDemoDatais gated oncompany.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
PlatformUsersControllerprotecting it are intentional, never remove them.
Implemented Modules
All fully implemented with controllers, views, and migrations applied.
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)
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)
Shop Management: Shop Workers (roles, job/maintenance assignment) · Equipment & Maintenance · Catalog Items · Pricing Tiers
Billing: Stripe (subscriptions, checkout sessions, webhooks /stripe/webhook) · Stripe Connect (embedded payments, OAuth) · Twilio SMS (ISmsService; webhook POST /Webhooks/TwilioSms)
Platform (SuperAdmin): Platform Users · Companies · Seed Data (manual only) · Subscription Plans (SubscriptionPlanConfig)
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
companylogoscontainer; 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.