# 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

```bash
# 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.

```bash
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.
- **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: 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 CRUD** → `IUnitOfWork.EntityName` (generic `IRepository<T>`)
```csharp
var items = await _unitOfWork.CatalogItems.GetAllAsync();
await _unitOfWork.Announcements.AddAsync(entity);
await _unitOfWork.CompleteAsync();
```

**Tier 2 — Complex domain queries** → typed repositories on `IUnitOfWork`
```csharp
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
```csharp
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 1–3 are done — intentional

---

## Configuration

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

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