using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Shared.Constants; namespace PowderCoating.Infrastructure.Data; public static class SeedData { public static DateTime? LastSeedRun { get; private set; } public static async Task InitializeAsync( IServiceProvider serviceProvider, UserManager userManager, RoleManager roleManager) { LastSeedRun = DateTime.UtcNow; var context = serviceProvider.GetRequiredService(); // Seed default company first var defaultCompany = await SeedDefaultCompanyAsync(context); // Seed roles await SeedRolesAsync(roleManager); // Seed admin users await SeedAdminUsersAsync(userManager, defaultCompany); // Seed inventory items (powder coating) await SeedInventoryItemsAsync(context, defaultCompany); // Seed operating costs await SeedOperatingCostsAsync(context, defaultCompany); // Seed notification templates await SeedNotificationTemplatesAsync(context, defaultCompany); // Seed subscription plan configs (global, only if table is empty) await SeedSubscriptionPlanConfigsAsync(context); // Seed default Chart of Accounts for the demo company await SeedChartOfAccountsAsync(context, defaultCompany); // Seed platform release notes (global, only if table is empty) await SeedReleaseNotesAsync(context); // Note: Catalog data is NOT seeded - users create categories and items on-the-fly per company } private static async Task SeedReleaseNotesAsync(ApplicationDbContext context) { if (await context.ReleaseNotes.AnyAsync()) return; var notes = new List { // ── v1.4 – March 2026 ──────────────────────────────────────────── new ReleaseNote { Version = "1.4.0", Title = "Deposits, Merchandise & Custom Powder Ordering", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 29, 0, 0, 0, DateTimeKind.Utc), Body = """ ## Deposits You can now record deposits against any customer, job, or quote directly from the job or quote detail page. Deposits are automatically applied to the invoice when it is created, reducing the amount due with no manual math required. Each deposit generates a printable PDF receipt. ## Merchandise / Sales Items A new **Sales Item** type has been added to the quoting and job wizard. Use it to add off-the-shelf products — touch-up paint, hardware, accessories — directly to a quote or job at a fixed price. Sales items are not run through the coating pricing engine. ## Custom Powder Order Pricing When a coat requires powder that must be purchased specifically for the job, the quote now charges for the **full quantity ordered**, not just the estimated usage. If you need 1.5 lbs but the smallest bag is 3 lbs, the customer is charged for all 3 lbs — accurately reflecting your actual material cost for that job. ## Gift Certificate Redemption on Invoices Gift certificates can now be applied directly as a line item when creating an invoice. """ }, new ReleaseNote { Version = "1.4.0", Title = "Tax Exempt Customers on Quotes and Invoices", Tag = "Fix", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 29, 0, 0, 0, DateTimeKind.Utc), Body = """ Quotes and invoices created for tax exempt customers now correctly default the tax rate to 0%. Previously, the company tax rate was applied even when the customer was marked tax exempt. Tax exempt customers are now marked with a ★ in the customer dropdown so they are easy to identify at a glance. """ }, // ── v1.3 – March 2026 ──────────────────────────────────────────── new ReleaseNote { Version = "1.3.0", Title = "Invoicing & Payment Tracking", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 20, 0, 0, 0, DateTimeKind.Utc), Body = """ ## Full Invoicing Module Invoices can now be created directly from a completed job with a single click. All line items, pricing, and customer information carry over automatically. **Key capabilities:** - Create, send, and download invoices as PDF - Record multiple partial payments against a single invoice (cash, check, card, ACH, and more) - Void invoices when needed — customer balances update automatically - Invoice numbering follows the `INV-YYMM-####` format for easy filing - Each job can have exactly one invoice, enforced by the system to prevent duplicates ## Customer Running Balance Customer account balances are now maintained automatically as invoices are created and payments recorded. You can see the current balance on the customer detail page at any time. ## Financial Reports A new **Financial** tab in Reports includes: - Monthly invoiced vs. collected comparison chart - Accounts receivable aging (current, 30, 60, 90+ days) - Top outstanding customer balances - Recent payment history """ }, new ReleaseNote { Version = "1.3.0", Title = "Online Payment Acceptance", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 20, 0, 0, 0, DateTimeKind.Utc), Body = """ Customers can now pay invoices online via credit or debit card through Stripe. When you send an invoice email, the customer receives a secure payment link. Payments are recorded automatically once completed — no manual entry required. """ }, new ReleaseNote { Version = "1.3.0", Title = "Refunds & Credit Memos", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 18, 0, 0, 0, DateTimeKind.Utc), Body = """ You can now issue refunds and credit memos against paid invoices. Credits are tracked on the customer account and can be applied to future invoices. """ }, // ── v1.2 – March 2026 ──────────────────────────────────────────── new ReleaseNote { Version = "1.2.0", Title = "AI Photo Quoting", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 17, 0, 0, 0, DateTimeKind.Utc), Body = """ ## Quote Items from Photos The quote wizard now includes an **AI Photo Quote** item type. Upload one or more photos of the part, and the AI will estimate the surface area, coating complexity, and suggested price — in seconds. **How it works:** 1. Add a new item to your quote and choose **AI Photo Quote** 2. Upload photos of the part (drag and drop or click to browse) 3. The AI analyzes the images and returns an estimate with a confidence rating 4. Ask follow-up questions if needed — the AI supports back-and-forth conversation 5. Review and adjust the estimate, then accept it into your quote Estimates include surface area (sq ft), complexity rating, estimated coating time, and a suggested unit price. You can override any value before accepting. ## Per-Company AI Learning The AI learns from your shop's pricing over time. As you accept and adjust estimates, the system builds a profile of your pricing style and feeds it back into future estimates so they get closer to your actual numbers. """ }, new ReleaseNote { Version = "1.2.0", Title = "Job Templates", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 19, 0, 0, 0, DateTimeKind.Utc), Body = """ Do you coat the same parts repeatedly for the same customers? Save any job as a template and use it as a starting point for new jobs. All line items, coatings, and prep services are copied over — just update the customer and due date. """ }, new ReleaseNote { Version = "1.2.0", Title = "Gift Certificates", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 19, 0, 0, 0, DateTimeKind.Utc), Body = """ You can now issue gift certificates to customers. Each certificate has a unique code, a dollar value, and an optional expiration date. Balances are tracked as certificates are redeemed and can be applied to invoices. """ }, new ReleaseNote { Version = "1.2.0", Title = "Job Time Entries & Rework Tracking", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 18, 0, 0, 0, DateTimeKind.Utc), Body = """ ## Time Tracking Workers can now log actual time against a job at each stage. Time entries give you visibility into real labor costs vs. the estimated hours on the quote — and help identify where jobs consistently run over. ## Rework Tracking Jobs can be flagged as rework and linked to the original job. Rework cost is tracked separately so you can see the true cost of quality issues over time. """ }, // ── v1.1 – March 2026 ──────────────────────────────────────────── new ReleaseNote { Version = "1.1.0", Title = "Oven Scheduler", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 16, 0, 0, 0, DateTimeKind.Utc), Body = """ The **Oven Scheduler** gives you a visual board to plan and manage oven batches. Drag pending job items into a batch, see capacity utilization, and track which batches are in progress vs. complete. Named ovens defined in your operating costs (with capacity in sq ft and cycle time) are used to calculate how much of each oven's capacity a batch will use. The scheduler can also suggest optimal batches automatically based on pending work. """ }, new ReleaseNote { Version = "1.1.0", Title = "Multi-Coat Quoting", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 16, 0, 0, 0, DateTimeKind.Utc), Body = """ Quote items now support multiple coating layers. Add a primer, a base coat, and a clear coat as separate line items — each with its own powder selection, coverage rate, and transfer efficiency. The pricing engine calculates material cost for each layer independently and adds an additional coat labor charge for layers 2 and up (configurable in operating costs). Powder-needed estimates display live in the wizard as you fill in surface area and coating details, so you know exactly how much to order before you confirm the quote. """ }, new ReleaseNote { Version = "1.1.0", Title = "Customer Quote Approval Portal", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 16, 0, 0, 0, DateTimeKind.Utc), Body = """ Send customers a secure link to review and approve their quote — no login required on their end. Customers see a clean summary of the work, pricing, and terms, and can approve or decline with a single click. You receive a notification as soon as they respond, and approved quotes can be converted to jobs immediately. """ }, new ReleaseNote { Version = "1.1.0", Title = "Pricing Tiers & Customer Discounts", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 16, 0, 0, 0, DateTimeKind.Utc), Body = """ Create named pricing tiers (e.g., Wholesale, Preferred, VIP) with a discount percentage, then assign a tier to any commercial customer. The discount is applied automatically every time a quote is built for that customer — no need to remember to add it manually. """ }, // ── v1.0 – March 2026 ──────────────────────────────────────────── new ReleaseNote { Version = "1.0.0", Title = "Initial Release", Tag = "Feature", IsPublished = true, ReleasedAt = new DateTime(2026, 3, 16, 0, 0, 0, DateTimeKind.Utc), Body = """ Welcome to **Powder Coating Logix** — a purpose-built shop management system for powder coating businesses. ## What's included in v1.0 **Customers** Manage commercial and non-commercial customers, set credit limits, track tax exempt status, and view the full history of their jobs and quotes in one place. **Jobs** Track every job from the moment it comes in the door through 16 workflow stages: Pending → Quoted → Approved → Prep → Sandblasting → Masking → Cleaning → Oven → Coating → Curing → Quality Check → Completed → Ready for Pickup → Delivered. Jobs can be assigned to shop workers and flagged with priority levels (Normal, High, Urgent, Rush). **Quoting Engine** Build detailed quotes with calculated pricing based on your actual operating costs — labor rates, powder cost per lb, coverage rates, transfer efficiency, oven time, booth time, shop supplies, and your markup. Supports catalog items, custom work items, labor-only items, and prospect quotes (no customer account required). **Inventory** Track your powders, primers, and supplies with reorder point alerts. Every stock movement — purchases, usage, adjustments — is recorded as a transaction for a full audit trail. **Vendors & Purchase Orders** Manage the suppliers you buy from, create purchase orders, track receipt, and convert received POs to vendor bills in accounts payable. **Equipment & Maintenance** Record your oven, sandblaster, coating booth, and other equipment. Log maintenance records, schedule upcoming service, and track equipment status so nothing catches you off guard. **Shop Workers** Add floor staff, assign roles (Coater, Sandblaster, Masker, Oven Operator, etc.), and assign workers to jobs and maintenance tasks. **Reports** Financial summaries, job throughput, AR aging, powder usage, inventory levels, and more — with PDF export for the reports you need to share. **Setup Wizard** New accounts walk through an 18-step setup wizard to configure company information, operating costs, equipment, pricing, and team members. Every step can be revisited later from Company Settings. """ } }; context.ReleaseNotes.AddRange(notes); await context.SaveChangesAsync(); } private static async Task SeedDefaultCompanyAsync(ApplicationDbContext context) { // Check if default company already exists var defaultCompany = await context.Companies .IgnoreQueryFilters() .FirstOrDefaultAsync(c => c.CompanyCode == "DEMO"); if (defaultCompany == null) { defaultCompany = new Company { CompanyName = "Demo Company", CompanyCode = "DEMO", PrimaryContactName = "Admin User", PrimaryContactEmail = "demo@powdercoatinglogix.com", Phone = "(555) 123-4567", Address = "123 Demo Street", City = "Demo City", State = "CA", ZipCode = "90210", IsActive = true, SubscriptionStartDate = DateTime.UtcNow, SubscriptionPlan = 2, // Enterprise TimeZone = "America/New_York", CreatedAt = DateTime.UtcNow, CompanyId = 0 // Self-referencing, will be set after creation }; await context.Companies.AddAsync(defaultCompany); await context.SaveChangesAsync(); // Update CompanyId to self-reference defaultCompany.CompanyId = defaultCompany.Id; await context.SaveChangesAsync(); } return defaultCompany; } private static async Task SeedRolesAsync(RoleManager roleManager) { string[] roles = new[] { AppConstants.Roles.SuperAdmin, AppConstants.Roles.Administrator, AppConstants.Roles.Manager, AppConstants.Roles.Employee, AppConstants.Roles.ShopFloor, AppConstants.Roles.ReadOnly }; foreach (var role in roles) { if (!await roleManager.RoleExistsAsync(role)) { await roleManager.CreateAsync(new IdentityRole(role)); } } } private static async Task SeedAdminUsersAsync(UserManager userManager, Company defaultCompany) { // Create SuperAdmin user const string superAdminEmail = "superadmin@powdercoatinglogix.com"; const string superAdminPassword = "SuperAdmin123!"; var superAdmin = await userManager.FindByEmailAsync(superAdminEmail); if (superAdmin == null) { superAdmin = new ApplicationUser { UserName = superAdminEmail, Email = superAdminEmail, FirstName = "Super", LastName = "Admin", EmployeeNumber = "SA-001", EmailConfirmed = true, HireDate = DateTime.UtcNow, IsActive = true, Department = "Platform", Position = "Super Administrator", CompanyId = defaultCompany.Id, CompanyRole = null, // SuperAdmin doesn't have a company role CanManageJobs = true, CanManageInventory = true, CanManageCustomers = true, CanCreateQuotes = true, CanApproveQuotes = true, CanManageCalendar = true, CanManageProducts = true, CanManageEquipment = true, CanManageVendors = true, CanManageMaintenance = true }; var result = await userManager.CreateAsync(superAdmin, superAdminPassword); if (result.Succeeded) { await userManager.AddToRoleAsync(superAdmin, AppConstants.Roles.SuperAdmin); } } // Create a second SuperAdmin user to demonstrate multi-superadmin support const string superAdmin2Email = "spouliot@powdercoatinglogix.com"; const string superAdmin2Password = "SuperAdmin123!"; var superAdmin2 = await userManager.FindByEmailAsync(superAdmin2Email); if (superAdmin2 == null) { superAdmin2 = new ApplicationUser { UserName = superAdmin2Email, Email = superAdmin2Email, FirstName = "Scott", LastName = "Pouliot", EmployeeNumber = "SA-002", EmailConfirmed = true, HireDate = DateTime.UtcNow, IsActive = true, Department = "Platform", Position = "Platform Administrator", CompanyId = defaultCompany.Id, CompanyRole = null, // SuperAdmin doesn't have a company role CanManageJobs = true, CanManageInventory = true, CanManageCustomers = true, CanCreateQuotes = true, CanApproveQuotes = true, CanManageCalendar = true, CanManageProducts = true, CanManageEquipment = true, CanManageVendors = true, CanManageMaintenance = true }; var result2 = await userManager.CreateAsync(superAdmin2, superAdmin2Password); if (result2.Succeeded) { await userManager.AddToRoleAsync(superAdmin2, AppConstants.Roles.SuperAdmin); } } // Create Company Admin user for demo company const string companyAdminEmail = "demo@powdercoatinglogix.com"; const string companyAdminPassword = "CompanyAdmin123!"; var companyAdmin = await userManager.FindByEmailAsync(companyAdminEmail); if (companyAdmin == null) { companyAdmin = new ApplicationUser { UserName = companyAdminEmail, Email = companyAdminEmail, FirstName = "Company", LastName = "Admin", EmployeeNumber = "CA-001", EmailConfirmed = true, HireDate = DateTime.UtcNow, IsActive = true, Department = "Management", Position = "Company Administrator", CompanyId = defaultCompany.Id, CompanyRole = AppConstants.CompanyRoles.CompanyAdmin, CanManageJobs = true, CanManageInventory = true, CanManageCustomers = true, CanCreateQuotes = true, CanApproveQuotes = true, CanManageCalendar = true, CanManageProducts = true, CanManageEquipment = true, CanManageVendors = true, CanManageMaintenance = true }; var result = await userManager.CreateAsync(companyAdmin, companyAdminPassword); if (result.Succeeded) { // Legacy role for backward compatibility await userManager.AddToRoleAsync(companyAdmin, AppConstants.Roles.Administrator); } } // Create a Manager user for demo company const string managerEmail = "manager@demo.com"; const string managerPassword = "Manager123!"; var manager = await userManager.FindByEmailAsync(managerEmail); if (manager == null) { manager = new ApplicationUser { UserName = managerEmail, Email = managerEmail, FirstName = "Demo", LastName = "Manager", EmployeeNumber = "MGR-001", EmailConfirmed = true, HireDate = DateTime.UtcNow, IsActive = true, Department = "Operations", Position = "Operations Manager", CompanyId = defaultCompany.Id, CompanyRole = AppConstants.CompanyRoles.Manager, CanManageJobs = true, CanManageInventory = true, CanManageCustomers = true, CanCreateQuotes = true, CanApproveQuotes = false }; var result = await userManager.CreateAsync(manager, managerPassword); if (result.Succeeded) { await userManager.AddToRoleAsync(manager, AppConstants.Roles.Manager); } } } private static async Task SeedInventoryItemsAsync(ApplicationDbContext context, Company defaultCompany) { // Check if inventory items already exist var existingItems = await context.InventoryItems .IgnoreQueryFilters() .AnyAsync(i => i.CompanyId == defaultCompany.Id); if (existingItems) { return; // Already seeded } var inventoryItems = new List { // Powder Coating Inventory new InventoryItem { SKU = "PWD-BLK-001", Name = "Matte Black Powder", Description = "High-quality matte black powder coating", Category = "Powder", ColorName = "Matte Black", ColorCode = "RAL 9005", Finish = "Matte", Manufacturer = "Tiger Drylac", ManufacturerPartNumber = "TG-MB-001", QuantityOnHand = 500, UnitOfMeasure = "lbs", ReorderPoint = 100, ReorderQuantity = 250, MinimumStock = 50, MaximumStock = 1000, UnitCost = 4.50m, AverageCost = 4.50m, LastPurchasePrice = 4.50m, LastPurchaseDate = DateTime.UtcNow.AddDays(-30), CoverageSqFtPerLb = 30m, TransferEfficiency = 65m, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new InventoryItem { SKU = "PWD-WHT-001", Name = "Gloss White Powder", Description = "High-gloss white powder coating", Category = "Powder", ColorName = "Gloss White", ColorCode = "RAL 9010", Finish = "Gloss", Manufacturer = "Tiger Drylac", ManufacturerPartNumber = "TG-GW-001", QuantityOnHand = 400, UnitOfMeasure = "lbs", ReorderPoint = 100, ReorderQuantity = 250, MinimumStock = 50, MaximumStock = 1000, UnitCost = 4.25m, AverageCost = 4.25m, LastPurchasePrice = 4.25m, LastPurchaseDate = DateTime.UtcNow.AddDays(-25), CoverageSqFtPerLb = 30m, TransferEfficiency = 65m, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new InventoryItem { SKU = "PWD-RED-001", Name = "Gloss Red Powder", Description = "Vibrant gloss red powder coating", Category = "Powder", ColorName = "Traffic Red", ColorCode = "RAL 3020", Finish = "Gloss", Manufacturer = "Tiger Drylac", ManufacturerPartNumber = "TG-GR-001", QuantityOnHand = 150, UnitOfMeasure = "lbs", ReorderPoint = 50, ReorderQuantity = 100, MinimumStock = 25, MaximumStock = 500, UnitCost = 5.75m, AverageCost = 5.75m, LastPurchasePrice = 5.75m, LastPurchaseDate = DateTime.UtcNow.AddDays(-20), CoverageSqFtPerLb = 30m, TransferEfficiency = 65m, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new InventoryItem { SKU = "PWD-BLU-001", Name = "Metallic Blue Powder", Description = "Metallic blue powder coating with shimmer", Category = "Powder", ColorName = "Metallic Blue", ColorCode = "RAL 5002", Finish = "Metallic", Manufacturer = "Axalta", ManufacturerPartNumber = "AX-MB-001", QuantityOnHand = 200, UnitOfMeasure = "lbs", ReorderPoint = 75, ReorderQuantity = 150, MinimumStock = 25, MaximumStock = 500, UnitCost = 6.25m, AverageCost = 6.25m, LastPurchasePrice = 6.25m, LastPurchaseDate = DateTime.UtcNow.AddDays(-15), CoverageSqFtPerLb = 30m, TransferEfficiency = 65m, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new InventoryItem { SKU = "PWD-GRY-001", Name = "Textured Gray Powder", Description = "Textured gray powder coating for industrial use", Category = "Powder", ColorName = "Textured Gray", ColorCode = "RAL 7037", Finish = "Textured", Manufacturer = "Axalta", ManufacturerPartNumber = "AX-TG-001", QuantityOnHand = 300, UnitOfMeasure = "lbs", ReorderPoint = 75, ReorderQuantity = 150, MinimumStock = 50, MaximumStock = 600, UnitCost = 5.00m, AverageCost = 5.00m, LastPurchasePrice = 5.00m, LastPurchaseDate = DateTime.UtcNow.AddDays(-10), CoverageSqFtPerLb = 30m, TransferEfficiency = 65m, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new InventoryItem { SKU = "PWD-YEL-001", Name = "Safety Yellow Powder", Description = "High-visibility safety yellow powder coating", Category = "Powder", ColorName = "Safety Yellow", ColorCode = "RAL 1003", Finish = "Gloss", Manufacturer = "Tiger Drylac", ManufacturerPartNumber = "TG-SY-001", QuantityOnHand = 125, UnitOfMeasure = "lbs", ReorderPoint = 50, ReorderQuantity = 100, MinimumStock = 25, MaximumStock = 400, UnitCost = 5.50m, AverageCost = 5.50m, LastPurchasePrice = 5.50m, LastPurchaseDate = DateTime.UtcNow.AddDays(-5), CoverageSqFtPerLb = 30m, TransferEfficiency = 65m, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new InventoryItem { SKU = "PWD-ORG-001", Name = "Orange Powder", Description = "Bright orange powder coating", Category = "Powder", ColorName = "Pure Orange", ColorCode = "RAL 2004", Finish = "Gloss", Manufacturer = "Axalta", ManufacturerPartNumber = "AX-PO-001", QuantityOnHand = 100, UnitOfMeasure = "lbs", ReorderPoint = 40, ReorderQuantity = 80, MinimumStock = 20, MaximumStock = 300, UnitCost = 5.85m, AverageCost = 5.85m, LastPurchasePrice = 5.85m, LastPurchaseDate = DateTime.UtcNow.AddDays(-12), CoverageSqFtPerLb = 30m, TransferEfficiency = 65m, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new InventoryItem { SKU = "PWD-GRN-001", Name = "Forest Green Powder", Description = "Deep forest green powder coating", Category = "Powder", ColorName = "Forest Green", ColorCode = "RAL 6009", Finish = "Matte", Manufacturer = "Tiger Drylac", ManufacturerPartNumber = "TG-FG-001", QuantityOnHand = 175, UnitOfMeasure = "lbs", ReorderPoint = 60, ReorderQuantity = 120, MinimumStock = 30, MaximumStock = 400, UnitCost = 5.25m, AverageCost = 5.25m, LastPurchasePrice = 5.25m, LastPurchaseDate = DateTime.UtcNow.AddDays(-8), CoverageSqFtPerLb = 30m, TransferEfficiency = 65m, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, // Other categories new InventoryItem { SKU = "CLN-001", Name = "Pre-Treatment Cleaner", Description = "Industrial degreaser and cleaner", Category = "Cleaner", QuantityOnHand = 50, UnitOfMeasure = "gallons", ReorderPoint = 10, ReorderQuantity = 25, MinimumStock = 5, MaximumStock = 100, UnitCost = 12.50m, AverageCost = 12.50m, LastPurchasePrice = 12.50m, LastPurchaseDate = DateTime.UtcNow.AddDays(-20), IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new InventoryItem { SKU = "MSK-001", Name = "High-Temp Masking Tape", Description = "Heat-resistant masking tape for powder coating", Category = "Masking", QuantityOnHand = 200, UnitOfMeasure = "rolls", ReorderPoint = 50, ReorderQuantity = 100, MinimumStock = 25, MaximumStock = 500, UnitCost = 8.75m, AverageCost = 8.75m, LastPurchasePrice = 8.75m, LastPurchaseDate = DateTime.UtcNow.AddDays(-15), IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow } }; await context.InventoryItems.AddRangeAsync(inventoryItems); await context.SaveChangesAsync(); } private static async Task SeedOperatingCostsAsync(ApplicationDbContext context, Company defaultCompany) { // Check if operating costs already exist var existingCosts = await context.CompanyOperatingCosts .IgnoreQueryFilters() .FirstOrDefaultAsync(c => c.CompanyId == defaultCompany.Id); if (existingCosts != null) { return; // Already seeded } var operatingCosts = new CompanyOperatingCosts { CompanyId = defaultCompany.Id, // Labor Costs (per hour) StandardLaborRate = 25.00m, // Equipment Costs (per hour) OvenOperatingCostPerHour = 15.00m, SandblasterCostPerHour = 12.00m, CoatingBoothCostPerHour = 10.00m, // Material Costs PowderCoatingCostPerSqFt = 0.85m, // Markup & Profit GeneralMarkupPercentage = 30.00m, // Rush Charge RushChargeType = "Percentage", RushChargePercentage = 25.00m, RushChargeFixedAmount = 0m, // Tax TaxPercent = 7.50m, CreatedAt = DateTime.UtcNow }; await context.CompanyOperatingCosts.AddAsync(operatingCosts); await context.SaveChangesAsync(); } private static async Task SeedNotificationTemplatesAsync(ApplicationDbContext context, Company company) { // Skip if any templates already exist for this company var exists = await context.NotificationTemplates .IgnoreQueryFilters() .AnyAsync(t => t.CompanyId == company.Id); if (exists) return; var templates = BuildDefaultNotificationTemplates(company.Id); await context.NotificationTemplates.AddRangeAsync(templates); await context.SaveChangesAsync(); } /// /// Returns the canonical default notification templates for a company. /// Called by both SeedData and CompanySettingsController for auto-seeding. /// public static List BuildDefaultNotificationTemplates(int companyId) { return [ new NotificationTemplate { NotificationType = NotificationType.QuoteSent, Channel = NotificationChannel.Email, DisplayName = "Quote Sent", Subject = "Your Quote {{quoteNumber}} from {{companyName}}", Body = "

Dear {{customerName}},

Thank you for your interest. Quote {{quoteNumber}} for {{quoteTotal}}{{quoteExpiry}} has been prepared for your review.

View & Approve Your Quote

Or copy this link: {{approvalUrl}}

Thank you for choosing {{companyName}}.

", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.QuoteApproved, Channel = NotificationChannel.Email, DisplayName = "Quote Approved", Subject = "Quote {{quoteNumber}} Approved — {{companyName}}", Body = "

Dear {{customerName}},

Your quote {{quoteNumber}} has been approved. We'll be in touch to schedule the work.

Thank you for choosing {{companyName}}.

", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.JobStatusChanged, Channel = NotificationChannel.Email, DisplayName = "Job Status Updated", Subject = "Job {{jobNumber}} Status Update — {{companyName}}", Body = "

Dear {{customerName}},

Your job {{jobNumber}} status is now: {{jobStatus}}.{{jobDueDate}}

Thank you for choosing {{companyName}}.

", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.JobReadyForPickup, Channel = NotificationChannel.Email, DisplayName = "Job Ready for Pickup", Subject = "Job {{jobNumber}} Ready for Pickup — {{companyName}}", Body = "

Dear {{customerName}},

Your job {{jobNumber}} is ready for pickup!

Thank you for choosing {{companyName}}.

", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.JobCompleted, Channel = NotificationChannel.Email, DisplayName = "Job Completed", Subject = "Job {{jobNumber}} Complete — {{companyName}}", Body = "

Dear {{customerName}},

Your job {{jobNumber}} is complete. Final price: {{finalPrice}}. It is now ready for pickup.

Thank you for choosing {{companyName}}.

", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.JobCompleted, Channel = NotificationChannel.Sms, DisplayName = "Job Completed (SMS)", Subject = null, Body = "{{companyName}}: Job {{jobNumber}} is done and ready for pickup! Reply STOP to opt out.", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.QuoteSent, Channel = NotificationChannel.Sms, DisplayName = "Quote Sent (SMS)", Subject = null, Body = "{{companyName}}: Quote {{quoteNumber}} for {{quoteTotal}} is ready for your review. Approve or decline: {{approvalUrl}} Reply STOP to opt out.", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.SmsConsentConfirmation, Channel = NotificationChannel.Sms, DisplayName = "SMS Enrollment Confirmation", Subject = null, Body = "{{companyName}}: You're now enrolled for SMS job updates and quote approvals. Reply STOP at any time to opt out. Msg & data rates may apply.", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.InvoiceSent, Channel = NotificationChannel.Email, DisplayName = "Invoice Sent", Subject = "Invoice {{invoiceNumber}} from {{companyName}}", Body = "

Dear {{customerName}},

Please find your invoice {{invoiceNumber}} for {{invoiceTotal}} attached.{{invoiceDueDate}}

Thank you for your business with {{companyName}}.

", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.InvoiceSent, Channel = NotificationChannel.Sms, DisplayName = "Invoice Sent (SMS)", Subject = null, Body = "{{companyName}}: Invoice {{invoiceNumber}} for {{invoiceTotal}} is ready. View your invoice: {{viewUrl}} Reply STOP to opt out.", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.PaymentReceived, Channel = NotificationChannel.Email, DisplayName = "Payment Received", Subject = "Payment Received — Invoice {{invoiceNumber}}", Body = "

Dear {{customerName}},

We have received your payment of {{paymentAmount}} on {{paymentDate}} for invoice {{invoiceNumber}}.{{balanceDue}}

Thank you for your business with {{companyName}}.

", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.QuoteApprovedByCustomer, Channel = NotificationChannel.Email, DisplayName = "Quote Approved by Customer (Internal)", Subject = "Customer Response: Quote {{quoteNumber}} — {{companyName}}", Body = "

Hello,

A customer has responded to quote {{quoteNumber}}.

Customer: {{customerName}}
Response: {{response}}

Log in to the portal to review and follow up.

", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, new NotificationTemplate { NotificationType = NotificationType.QuoteDeclinedByCustomer, Channel = NotificationChannel.Email, DisplayName = "Quote Declined by Customer (Internal)", Subject = "Customer Response: Quote {{quoteNumber}} — {{companyName}}", Body = "

Hello,

A customer has responded to quote {{quoteNumber}}.

Customer: {{customerName}}
Response: {{response}}

{{declineReasonSection}}

Log in to the portal to review and follow up.

", IsActive = true, CompanyId = companyId, CreatedAt = DateTime.UtcNow }, ]; } // Catalog seed data removed - categories and items are created by users per company /* private static async Task SeedCatalogDataAsync(ApplicationDbContext context, Company defaultCompany) { // Check if catalog data already exists var existingCategories = await context.CatalogCategories .IgnoreQueryFilters() .AnyAsync(c => c.CompanyId == defaultCompany.Id); if (existingCategories) { return; // Already seeded } // Create root categories var wheelsCategory = new CatalogCategory { Name = "Wheels", Description = "Automotive and motorcycle wheels", DisplayOrder = 1, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }; var partsCategory = new CatalogCategory { Name = "Engine Parts", Description = "Engine components and covers", DisplayOrder = 2, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }; var furnitureCategory = new CatalogCategory { Name = "Outdoor Furniture", Description = "Patio and garden furniture", DisplayOrder = 3, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }; await context.CatalogCategories.AddRangeAsync(wheelsCategory, partsCategory, furnitureCategory); await context.SaveChangesAsync(); // Create subcategories for Wheels var wheels18Category = new CatalogCategory { Name = "18 inch", Description = "18 inch diameter wheels", ParentCategoryId = wheelsCategory.Id, DisplayOrder = 1, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }; var wheels20Category = new CatalogCategory { Name = "20 inch", Description = "20 inch diameter wheels", ParentCategoryId = wheelsCategory.Id, DisplayOrder = 2, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }; await context.CatalogCategories.AddRangeAsync(wheels18Category, wheels20Category); await context.SaveChangesAsync(); // Create catalog items var catalogItems = new List { // 18 inch wheels new CatalogItem { Name = "Standard 18\" Aluminum Wheel", Description = "Standard aluminum alloy wheel, 18 inch diameter", SKU = "WHL-18-STD", CategoryId = wheels18Category.Id, DefaultPrice = 75.00m, DefaultRequiresSandblasting = true, DefaultRequiresMasking = false, DefaultEstimatedMinutes = 30, DisplayOrder = 1, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new CatalogItem { Name = "Chrome 18\" Wheel (Re-coating)", Description = "Chrome-plated 18 inch wheel, requires special prep", SKU = "WHL-18-CHR", CategoryId = wheels18Category.Id, DefaultPrice = 95.00m, DefaultRequiresSandblasting = true, DefaultRequiresMasking = true, DefaultEstimatedMinutes = 45, DisplayOrder = 2, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, // 20 inch wheels new CatalogItem { Name = "Standard 20\" Aluminum Wheel", Description = "Standard aluminum alloy wheel, 20 inch diameter", SKU = "WHL-20-STD", CategoryId = wheels20Category.Id, DefaultPrice = 85.00m, DefaultRequiresSandblasting = true, DefaultRequiresMasking = false, DefaultEstimatedMinutes = 35, DisplayOrder = 1, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, // Engine parts new CatalogItem { Name = "4-Cylinder Valve Cover", Description = "Valve cover for 4-cylinder engines", SKU = "ENG-VC-4CYL", CategoryId = partsCategory.Id, DefaultPrice = 45.00m, DefaultRequiresSandblasting = true, DefaultRequiresMasking = true, DefaultEstimatedMinutes = 45, DisplayOrder = 1, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new CatalogItem { Name = "6-Cylinder Valve Cover", Description = "Valve cover for 6-cylinder engines (larger)", SKU = "ENG-VC-6CYL", CategoryId = partsCategory.Id, DefaultPrice = 55.00m, DefaultRequiresSandblasting = true, DefaultRequiresMasking = true, DefaultEstimatedMinutes = 60, DisplayOrder = 2, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new CatalogItem { Name = "Intake Manifold", Description = "Engine intake manifold", SKU = "ENG-IM", CategoryId = partsCategory.Id, DefaultPrice = 65.00m, DefaultRequiresSandblasting = true, DefaultRequiresMasking = true, DefaultEstimatedMinutes = 75, DisplayOrder = 3, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, // Outdoor furniture new CatalogItem { Name = "Patio Chair Frame", Description = "Metal frame for patio chair", SKU = "FURN-CHAIR", CategoryId = furnitureCategory.Id, DefaultPrice = 40.00m, DefaultRequiresSandblasting = true, DefaultRequiresMasking = false, DefaultEstimatedMinutes = 30, DisplayOrder = 1, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow }, new CatalogItem { Name = "Patio Table Frame", Description = "Metal frame for outdoor dining table", SKU = "FURN-TABLE", CategoryId = furnitureCategory.Id, DefaultPrice = 90.00m, DefaultRequiresSandblasting = true, DefaultRequiresMasking = false, DefaultEstimatedMinutes = 60, DisplayOrder = 2, IsActive = true, CompanyId = defaultCompany.Id, CreatedAt = DateTime.UtcNow } }; await context.CatalogItems.AddRangeAsync(catalogItems); await context.SaveChangesAsync(); } */ private static async Task SeedSubscriptionPlanConfigsAsync(ApplicationDbContext context) { // Fix existing rows that have MaxQuotePhotos or MaxJobPhotos = 0 (incorrect default — should be -1 = unlimited) var rowsToFix = await context.SubscriptionPlanConfigs.IgnoreQueryFilters() .Where(c => c.MaxQuotePhotos == 0 || c.MaxJobPhotos == 0) .ToListAsync(); if (rowsToFix.Count > 0) { foreach (var row in rowsToFix) { if (row.MaxQuotePhotos == 0) row.MaxQuotePhotos = -1; if (row.MaxJobPhotos == 0) row.MaxJobPhotos = -1; } await context.SaveChangesAsync(); } // Enable AllowSms for Pro and Enterprise plans if not already set var smsPlansToFix = await context.SubscriptionPlanConfigs.IgnoreQueryFilters() .Where(c => (c.Plan == 1 || c.Plan == 2) && !c.AllowSms) .ToListAsync(); if (smsPlansToFix.Count > 0) { foreach (var row in smsPlansToFix) row.AllowSms = true; await context.SaveChangesAsync(); } // Only seed if table is empty if (await context.SubscriptionPlanConfigs.IgnoreQueryFilters().AnyAsync()) return; var configs = new List { new() { Plan = 3, // Starter DisplayName = "Starter", Description = "Try it out — ideal for solo operators or very small shops", MaxUsers = 1, MaxActiveJobs = 1, MaxCustomers = 25, MaxQuotes = 10, MaxCatalogItems = 25, MonthlyPrice = 19.99m, AnnualPrice = 199m, IsActive = true, SortOrder = 1, CompanyId = 0, CreatedAt = DateTime.UtcNow }, new() { Plan = 0, // Basic DisplayName = "Basic", Description = "Perfect for small shops getting started", MaxUsers = 3, MaxActiveJobs = 50, MaxCustomers = 100, MaxQuotes = 50, MaxCatalogItems = 100, MonthlyPrice = 29m, AnnualPrice = 290m, IsActive = true, SortOrder = 2, CompanyId = 0, CreatedAt = DateTime.UtcNow }, new() { Plan = 1, // Pro DisplayName = "Pro", Description = "For growing businesses with more capacity needs", MaxUsers = 10, MaxActiveJobs = 250, MaxCustomers = 1000, MaxQuotes = 500, MaxCatalogItems = 500, MonthlyPrice = 79m, AnnualPrice = 790m, AllowSms = true, IsActive = true, SortOrder = 3, CompanyId = 0, CreatedAt = DateTime.UtcNow }, new() { Plan = 2, // Enterprise DisplayName = "Enterprise", Description = "Unlimited capacity for large operations", MaxUsers = -1, MaxActiveJobs = -1, MaxCustomers = -1, MaxQuotes = -1, MaxCatalogItems = -1, MonthlyPrice = 199m, AnnualPrice = 1990m, AllowSms = true, IsActive = true, SortOrder = 4, CompanyId = 0, CreatedAt = DateTime.UtcNow } }; await context.SubscriptionPlanConfigs.AddRangeAsync(configs); await context.SaveChangesAsync(); } /// /// Seeds a default Chart of Accounts for a company if none exist yet. /// Safe to call for both the demo company and newly created companies. /// public static async Task SeedChartOfAccountsAsync(ApplicationDbContext context, Company company) { var alreadySeeded = await context.Accounts .IgnoreQueryFilters() .AnyAsync(a => a.CompanyId == company.Id && !a.IsDeleted); if (alreadySeeded) return; var now = DateTime.UtcNow; var accounts = new List { // ── Assets ────────────────────────────────────────────────────── new() { AccountNumber = "1000", Name = "Checking Account", AccountType = AccountType.Asset, AccountSubType = AccountSubType.Checking, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "1010", Name = "Savings Account", AccountType = AccountType.Asset, AccountSubType = AccountSubType.Savings, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "1100", Name = "Petty Cash", AccountType = AccountType.Asset, AccountSubType = AccountSubType.Checking, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "1200", Name = "Accounts Receivable", AccountType = AccountType.Asset, AccountSubType = AccountSubType.AccountsReceivable, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "1400", Name = "Inventory Asset", AccountType = AccountType.Asset, AccountSubType = AccountSubType.Inventory, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "1500", Name = "Equipment & Machinery", AccountType = AccountType.Asset, AccountSubType = AccountSubType.FixedAsset, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "1510", Name = "Vehicles", AccountType = AccountType.Asset, AccountSubType = AccountSubType.FixedAsset, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, // ── Liabilities ───────────────────────────────────────────────── new() { AccountNumber = "2000", Name = "Accounts Payable", AccountType = AccountType.Liability, AccountSubType = AccountSubType.AccountsPayable, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "2100", Name = "Credit Card", AccountType = AccountType.Liability, AccountSubType = AccountSubType.CreditCard, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "2200", Name = "Sales Tax Payable", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "2300", Name = "Payroll Liabilities", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "2500", Name = "Long-Term Loan", AccountType = AccountType.Liability, AccountSubType = AccountSubType.LongTermLiability, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, // ── Equity ────────────────────────────────────────────────────── new() { AccountNumber = "3000", Name = "Owner's Equity", AccountType = AccountType.Equity, AccountSubType = AccountSubType.OwnersEquity, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "3100", Name = "Retained Earnings", AccountType = AccountType.Equity, AccountSubType = AccountSubType.RetainedEarnings, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "3200", Name = "Owner's Draw", AccountType = AccountType.Equity, AccountSubType = AccountSubType.OwnersEquity, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, // ── Revenue ───────────────────────────────────────────────────── new() { AccountNumber = "4000", Name = "Sales Revenue", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.Sales, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "4100", Name = "Service Revenue", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.ServiceRevenue, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "4900", Name = "Other Income", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, // ── Cost of Goods Sold ─────────────────────────────────────────── new() { AccountNumber = "5000", Name = "Cost of Goods Sold", AccountType = AccountType.CostOfGoods, AccountSubType = AccountSubType.CostOfGoodsSold, IsSystem = true, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "5100", Name = "Powder & Materials", AccountType = AccountType.CostOfGoods, AccountSubType = AccountSubType.CostOfGoodsSold, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "5200", Name = "Direct Labor", AccountType = AccountType.CostOfGoods, AccountSubType = AccountSubType.CostOfGoodsSold, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, // ── Expenses ───────────────────────────────────────────────────── new() { AccountNumber = "6000", Name = "Rent & Lease", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Rent, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6100", Name = "Utilities", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Utilities, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6200", Name = "Supplies & Materials", AccountType = AccountType.Expense, AccountSubType = AccountSubType.SuppliesMaterials, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6300", Name = "Equipment & Tools", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Equipment, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6400", Name = "Payroll Expense", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Payroll, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6500", Name = "Insurance", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Insurance, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6600", Name = "Vehicle & Fuel", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Vehicle, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6700", Name = "Office Supplies", AccountType = AccountType.Expense, AccountSubType = AccountSubType.OfficeSupplies, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6800", Name = "Professional Services", AccountType = AccountType.Expense, AccountSubType = AccountSubType.ProfessionalFees, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6900", Name = "Advertising & Marketing", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Advertising, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6950", Name = "Bank Charges & Fees", AccountType = AccountType.Expense, AccountSubType = AccountSubType.BankCharges, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, new() { AccountNumber = "6990", Name = "Miscellaneous Expense", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Other, IsSystem = false, IsActive = true, CompanyId = company.Id, CreatedAt = now }, }; await context.Accounts.AddRangeAsync(accounts); await context.SaveChangesAsync(); } }