17 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code 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).
Essential Commands
Building and Running
# Build entire solution
dotnet build
# Run web application (MVC)
cd src/PowderCoating.Web
dotnet run
# Access at: https://localhost:58461
# Run web with auto-reload
dotnet watch 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
Database Operations
# 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
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 Overview
Clean Architecture Layers
Domain Layer (PowderCoating.Core)
- Contains business entities, enums, and repository interfaces
BaseEntityprovides 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
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)
ApplicationDbContextwith global query filters for soft deletes and multi-tenancy- Generic
Repository<T>implementingIRepository<T> UnitOfWorkimplementingIUnitOfWorkwith 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 UIPowderCoating.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()orCompleteAsync()to persist changes
Dependency Injection
- All dependencies registered in
Program.cs - Controllers inject
IUnitOfWorkandIMapper - 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: trueparameter in repository methods
Multi-Tenancy Implementation
CompanyIdforeign key on all business entitiesITenantContextinjected 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)
Data Access Patterns
Common Controller Pattern
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
// 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
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:
- Pending → Initial state
- Quoted → Quote generated
- Approved → Customer approved
- InPreparation → Job prep started
- Sandblasting → Surface prep
- MaskingTaping → Masking areas
- Cleaning → Pre-coat cleaning
- InOven → Pre-heating
- Coating → Applying powder
- Curing → Heat curing
- QualityCheck → Inspection
- Completed → Work finished
- ReadyForPickup → Awaiting customer
- Delivered → Job delivered
- OnHold → Paused
- 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
InventoryTransactionentity - 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)
{
"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)
{
"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:
RequireAdministratorRoleCanManageJobsCanManageInventoryCanManageUsersCanViewData
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:
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:
private readonly ILogger<ExampleController> _logger;
Common Development Tasks
Adding a New Entity
- Create entity class in
Core/Entities/inheriting fromBaseEntity - Add DbSet to
ApplicationDbContext - Register repository property in
IUnitOfWorkinterface - Add lazy-loaded property in
UnitOfWorkimplementation - Create migration:
dotnet ef migrations add AddEntityName --project ../PowderCoating.Infrastructure - Apply migration:
dotnet ef database update --project ../PowderCoating.Infrastructure
Adding a New Controller
- Create DTOs in
Application/DTOs/ - Create AutoMapper profile in
Application/Mappings/ - Create controller in
Web/Controllers/ - Create views in
Web/Views/[ControllerName]/ - Add navigation link in
Views/Shared/_Layout.cshtml
Working with Soft Deletes
// 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:
// 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
CompanyAdminOnlypolicy (notRequireAdministratorRole)
Billing & Payments
- Stripe — subscription plans, checkout sessions, customer portal, webhooks (
/stripe/webhook) - Stripe Connect — embedded payments, OAuth flow for tenant onboarding
- Twilio SMS —
ISmsServicefully implemented; webhook atPOST /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 —
SubscriptionPlanConfigcontrols 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):TaxPercentdefaults to 0 on quote and invoice create; customer dropdown marks exempt customers with ★
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
companylogoscontainer); it replaces the PCL logo in the sidebar header
Known Issues
- Entity Framework warnings about global query filters on related entities (non-critical, informational only)
File Upload Configuration
Limits defined in AppConstants.cs:
- Max file size: 10 MB
- Allowed extensions: 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
IUnitOfWorkin unit tests
Extending the System
Adding AI Features
AI uses Anthropic Claude Sonnet 4.6 via IAiQuoteService. Configure the key under AI:Anthropic:ApiKey in appsettings.json.
- Create service interface in
Application/Interfaces/ - Implement in
Infrastructure/Services/calling the Anthropic client - Inject into controllers via DI
SignalR Hubs
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:
- Create hub class in
Web/Hubs/ - Map hub in
Program.cs:app.MapHub<YourHub>("/hubpath") - Use JavaScript client in views to connect
Adding API Endpoints
- Create controller in
Api/Controllers/with[ApiController]attribute - Return
ActionResult<T>types - Use
[Authorize]for protected endpoints - Document with XML comments for Swagger
Project Dependencies
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)
Security Considerations
- 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
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.