Files
PowderCoatingLogix/AGENTS.md
T
2026-05-21 08:33:25 -04:00

21 KiB
Raw Permalink Blame History

AGENTS.md

This file provides guidance to Codex (Codex.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
  • 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

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)

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

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

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

Tier 3 — Aggregate/reporting queries → injected read services

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

Permanent exceptions (ApplicationDbContext allowed — intentional, documented):

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.


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.)
// 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:

  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)

{
  "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 Codex Sonnet 4.6 (Codex-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:

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

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

  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

// 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 Codex 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 SMSISmsService 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 PlansSubscriptionPlanConfig 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 ★

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.

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

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

  • 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 IUnitOfWork in unit tests

Extending the System

Adding AI Features

AI uses Anthropic Codex 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

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:

  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

Adding API Endpoints

  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

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/AGENTS.md for that work.