Files
PowderCoatingLogix/CLAUDE.md
T
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

11 KiB
Raw Blame History

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. 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.
  • InfrastructureApplicationDbContext, 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: true on 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)

ApplicationDbContext is NEVER injected into a controller. 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:

Tier 1 — Simple CRUDIUnitOfWork.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): 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 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:

  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

Configuration

Key Settings (src/PowderCoating.Web/appsettings.json)

  • DB: ConnectionStrings:DefaultConnection (SQL Server Express)
  • AI: AI:Anthropic:ApiKeyAnthropic Claude claude-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 — NOT RequireAdministratorRole (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&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).

Gotchas

  • 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.

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 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.