using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// /// Seeds a default chart of accounts for a newly onboarded company, covering all five /// standard double-entry categories: Assets, Liabilities, Equity, Revenue (with separate /// powder coating and sandblasting lines), Cost of Goods Sold, and Expenses. /// /// /// /// Idempotency: returns 0 immediately if any non-deleted accounts already exist for this /// company, so re-running seed does not produce duplicate account numbers. /// /// /// Several accounts are marked IsSystem = true (Checking, AR, AP, and main Powder /// Coating Revenue). System accounts are referenced by account number in other seeders /// (e.g. SeedInvoicesAsync looks up account "4000" for invoice line items) and /// should not be renamed or deleted by end users. /// /// /// Account numbers follow a conventional small-business numbering scheme: /// 1xxx = Assets, 2xxx = Liabilities, 3xxx = Equity, 4xxx = Revenue, /// 5xxx = Cost of Goods Sold, 6xxx = Expenses. This makes the chart immediately /// recognisable to accountants without custom configuration. /// /// /// The tenant company to seed the chart of accounts for. /// Number of account records inserted, or 0 if already seeded. private async Task SeedDefaultChartOfAccountsAsync(Company company) { var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(a => a.CompanyId == company.Id && !a.IsDeleted); if (existingCount > 0) return 0; // Already seeded var now = DateTime.UtcNow; var accounts = new List { // ── ASSETS ──────────────────────────────────────────────────────── // Opening balances represent accumulated cash before the 12-month seeded history window. // Without them, 12 months of seeded expenses outpace ~3 months of seeded revenue and // the checking account shows a large negative — unrealistic for a demo. new Account { AccountNumber = "1000", Name = "Checking Account", AccountType = AccountType.Asset, AccountSubType = AccountSubType.Checking, IsSystem = true, IsActive = true, Description = "Primary business checking account", OpeningBalance = 75_000m, OpeningBalanceDate = now.AddYears(-1), CurrentBalance = 75_000m, CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "1010", Name = "Savings Account", AccountType = AccountType.Asset, AccountSubType = AccountSubType.Savings, IsSystem = false, IsActive = true, Description = "Business savings account", OpeningBalance = 14_500m, OpeningBalanceDate = now.AddYears(-1), CurrentBalance = 14_500m, CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "1100", Name = "Accounts Receivable", AccountType = AccountType.Asset, AccountSubType = AccountSubType.AccountsReceivable, IsSystem = true, IsActive = true, Description = "Amounts owed by customers for services", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "1200", Name = "Inventory - Powder", AccountType = AccountType.Asset, AccountSubType = AccountSubType.Inventory, IsSystem = false, IsActive = true, Description = "Powder coating materials in stock", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "1210", Name = "Inventory - Consumables", AccountType = AccountType.Asset, AccountSubType = AccountSubType.Inventory, IsSystem = false, IsActive = true, Description = "Masking, tape, and other consumables", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "1300", Name = "Equipment", AccountType = AccountType.Asset, AccountSubType = AccountSubType.FixedAsset, IsSystem = false, IsActive = true, Description = "Ovens, booths, sandblasters, and equipment", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "1310", Name = "Accumulated Depreciation", AccountType = AccountType.Asset, AccountSubType = AccountSubType.FixedAsset, IsSystem = false, IsActive = true, Description = "Accumulated depreciation on equipment", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "1400", Name = "Prepaid Expenses", AccountType = AccountType.Asset, AccountSubType = AccountSubType.OtherCurrentAsset, IsSystem = false, IsActive = true, Description = "Prepaid insurance, rent, and other items", CompanyId = company.Id, CreatedAt = now }, // ── LIABILITIES ─────────────────────────────────────────────────── new Account { AccountNumber = "2000", Name = "Accounts Payable", AccountType = AccountType.Liability, AccountSubType = AccountSubType.AccountsPayable, IsSystem = true, IsActive = true, Description = "Amounts owed to suppliers and vendors", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "2100", Name = "Credit Card Payable", AccountType = AccountType.Liability, AccountSubType = AccountSubType.CreditCard, IsSystem = false, IsActive = true, Description = "Business credit card balance", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "2200", Name = "Sales Tax Payable", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = false, IsActive = true, Description = "Sales tax collected and owed to government", CompanyId = company.Id, CreatedAt = now }, // 2300 is the Customer Deposits liability — credited when a deposit is taken, debited when it is // applied to an invoice (see DepositsController / InvoicesController, which resolve it by number). // IsSystem because the GL posting code depends on it existing. Payroll lives at 2400 below. new Account { AccountNumber = "2300", Name = "Customer Deposits", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = true, IsActive = true, Description = "Deposits received from customers before an invoice is created; cleared when applied to an invoice", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "2350", Name = "Customer Credits", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = true, IsActive = true, Description = "Store credit owed to customers (credit memos not yet applied)", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "2400", Name = "Payroll Liabilities", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = false, IsActive = true, Description = "Payroll taxes and withholdings owed", CompanyId = company.Id, CreatedAt = now }, // 2500 Gift Certificate Liability — credited when a GC is issued, debited when redeemed/voided // (resolved by number in GiftCertificatesController). IsSystem because the GL posting depends on it. new Account { AccountNumber = "2500", Name = "Gift Certificate Liability", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = true, IsActive = true, Description = "Outstanding gift certificate obligations owed to certificate holders", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "2900", Name = "Business Loan", AccountType = AccountType.Liability, AccountSubType = AccountSubType.LongTermLiability, IsSystem = false, IsActive = true, Description = "Long-term equipment or business loan", CompanyId = company.Id, CreatedAt = now }, // ── EQUITY ──────────────────────────────────────────────────────── new Account { AccountNumber = "3000", Name = "Owner's Equity", AccountType = AccountType.Equity, AccountSubType = AccountSubType.OwnersEquity, IsSystem = false, IsActive = true, Description = "Owner's invested capital", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "3100", Name = "Retained Earnings", AccountType = AccountType.Equity, AccountSubType = AccountSubType.RetainedEarnings, IsSystem = false, IsActive = true, Description = "Cumulative net income retained in business", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "3200", Name = "Owner's Draw", AccountType = AccountType.Equity, AccountSubType = AccountSubType.OwnersEquity, IsSystem = false, IsActive = true, Description = "Withdrawals by owner", CompanyId = company.Id, CreatedAt = now }, // ── REVENUE ─────────────────────────────────────────────────────── new Account { AccountNumber = "4000", Name = "Powder Coating Revenue", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.Sales, IsSystem = true, IsActive = true, Description = "Revenue from powder coating services", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "4100", Name = "Sandblasting Revenue", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.ServiceRevenue, IsSystem = false, IsActive = true, Description = "Revenue from sandblasting services", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "4200", Name = "Other Service Revenue", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.ServiceRevenue, IsSystem = false, IsActive = true, Description = "Revenue from other shop services", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "4900", Name = "Other Income", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsSystem = false, IsActive = true, Description = "Miscellaneous income", CompanyId = company.Id, CreatedAt = now }, // Contra-revenue: debited when invoice discounts are applied so the GL balances. // A credit-normal account with a debit balance appears in the Trial Balance debit column, // reducing net revenue to match the discounted AR amount that was posted. new Account { AccountNumber = "4950", Name = "Sales Discounts", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsSystem = true, IsActive = true, Description = "Contra-revenue for invoice discounts granted to customers", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "4960", Name = "Sales Returns & Allowances", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsSystem = true, IsActive = true, Description = "Contra-revenue for the revenue portion of customer refunds (reversed sales)", CompanyId = company.Id, CreatedAt = now }, // ── COST OF GOODS SOLD ──────────────────────────────────────────── new Account { AccountNumber = "5000", Name = "Cost of Goods Sold", AccountType = AccountType.CostOfGoods, AccountSubType = AccountSubType.CostOfGoodsSold, IsSystem = false, IsActive = true, Description = "Direct cost of services delivered", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "5100", Name = "Powder & Materials", AccountType = AccountType.CostOfGoods, AccountSubType = AccountSubType.CostOfGoodsSold, IsSystem = false, IsActive = true, Description = "Powder coatings and direct materials used", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "5200", Name = "Consumables & Shop Supplies", AccountType = AccountType.CostOfGoods, AccountSubType = AccountSubType.CostOfGoodsSold, IsSystem = false, IsActive = true, Description = "Masking, tape, and other job supplies", CompanyId = company.Id, CreatedAt = now }, // ── EXPENSES ────────────────────────────────────────────────────── new Account { AccountNumber = "6000", Name = "Advertising & Marketing", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Advertising, IsSystem = false, IsActive = true, Description = "Advertising, marketing, and promotions", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6100", Name = "Equipment & Repairs", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Equipment, IsSystem = false, IsActive = true, Description = "Equipment maintenance and repair costs", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6200", Name = "Insurance", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Insurance, IsSystem = false, IsActive = true, Description = "Business, liability, and equipment insurance", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6300", Name = "Payroll & Labor", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Payroll, IsSystem = false, IsActive = true, Description = "Wages, salaries, and payroll taxes", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6400", Name = "Professional Fees", AccountType = AccountType.Expense, AccountSubType = AccountSubType.ProfessionalFees, IsSystem = false, IsActive = true, Description = "Accounting, legal, and consulting fees", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6500", Name = "Rent & Facilities", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Rent, IsSystem = false, IsActive = true, Description = "Shop rent and facility costs", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6600", Name = "Utilities", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Utilities, IsSystem = false, IsActive = true, Description = "Electric, gas, water, and internet", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6700", Name = "Vehicle & Transportation", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Vehicle, IsSystem = false, IsActive = true, Description = "Fuel, vehicle maintenance, and transport", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6800", Name = "Office Supplies", AccountType = AccountType.Expense, AccountSubType = AccountSubType.OfficeSupplies, IsSystem = false, IsActive = true, Description = "General office and administrative supplies", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6900", Name = "Bank Charges & Fees", AccountType = AccountType.Expense, AccountSubType = AccountSubType.BankCharges, IsSystem = false, IsActive = true, Description = "Bank fees, merchant processing, and wire fees", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6950", Name = "Depreciation Expense", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Depreciation, IsSystem = false, IsActive = true, Description = "Depreciation on fixed assets", CompanyId = company.Id, CreatedAt = now }, new Account { AccountNumber = "6990", Name = "Other Expenses", AccountType = AccountType.Expense, AccountSubType = AccountSubType.Other, IsSystem = false, IsActive = true, Description = "Miscellaneous business expenses", CompanyId = company.Id, CreatedAt = now }, }; await _context.Set().AddRangeAsync(accounts); await _context.SaveChangesAsync(); return accounts.Count; } /// /// Ensures system accounts introduced after the initial chart-of-accounts seed exist for the /// given company. Idempotent: each account is only inserted when absent, so this is safe to /// call repeatedly from the "Seed Lookup Tables" flow. /// Call this after so that newly onboarded /// companies get all accounts in one pass while existing companies receive only the missing ones. /// /// Number of accounts inserted (0 if all are already present). private async Task EnsureSystemAccountsAsync(Company company) { var now = DateTime.UtcNow; int added = 0; // 4950 Sales Discounts — contra-revenue account introduced to balance the GL when // invoice discounts are applied (DR Sales Discounts / CR Revenue gap fixed). var has4950 = await _context.Set() .IgnoreQueryFilters() .AnyAsync(a => a.CompanyId == company.Id && a.AccountNumber == "4950" && !a.IsDeleted); if (!has4950) { _context.Set().Add(new Account { AccountNumber = "4950", Name = "Sales Discounts", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsSystem = true, IsActive = true, Description = "Contra-revenue for invoice discounts granted to customers", CompanyId = company.Id, CreatedAt = now }); await _context.SaveChangesAsync(); added++; } // 4960 Sales Returns & Allowances — contra-revenue account that receives the revenue portion // of customer refunds under the "reverse the sale" model (DR Sales Returns + DR Sales Tax / CR Bank). var has4960 = await _context.Set() .IgnoreQueryFilters() .AnyAsync(a => a.CompanyId == company.Id && a.AccountNumber == "4960" && !a.IsDeleted); if (!has4960) { _context.Set().Add(new Account { AccountNumber = "4960", Name = "Sales Returns & Allowances", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsSystem = true, IsActive = true, Description = "Contra-revenue for the revenue portion of customer refunds (reversed sales)", CompanyId = company.Id, CreatedAt = now }); await _context.SaveChangesAsync(); added++; } // 2350 Customer Credits — liability for store credit owed to customers. Credited when a credit // memo (incl. store-credit refunds) is issued; debited when applied to an invoice. var has2350 = await _context.Set() .IgnoreQueryFilters() .AnyAsync(a => a.CompanyId == company.Id && a.AccountNumber == "2350" && !a.IsDeleted); if (!has2350) { _context.Set().Add(new Account { AccountNumber = "2350", Name = "Customer Credits", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = true, IsActive = true, Description = "Store credit owed to customers (credit memos not yet applied)", CompanyId = company.Id, CreatedAt = now }); await _context.SaveChangesAsync(); added++; } // 2300 used to be seeded as "Payroll Liabilities" but the deposit GL posting code has always // resolved 2300 by number and used it as the Customer Deposits liability — so the account was // mislabeled on the balance sheet. Rename it to "Customer Deposits" and mark it system. Only // touch accounts still carrying the old default name so a user's own rename is preserved. var legacyDepositsAcct = await _context.Set() .IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && a.AccountNumber == "2300" && !a.IsDeleted && a.Name == "Payroll Liabilities"); if (legacyDepositsAcct != null) { legacyDepositsAcct.Name = "Customer Deposits"; legacyDepositsAcct.Description = "Deposits received from customers before an invoice is created; cleared when applied to an invoice"; legacyDepositsAcct.IsSystem = true; legacyDepositsAcct.UpdatedAt = now; await _context.SaveChangesAsync(); } // 2400 Payroll Liabilities — the payroll account displaced from 2300 (now Customer Deposits). var has2400 = await _context.Set() .IgnoreQueryFilters() .AnyAsync(a => a.CompanyId == company.Id && a.AccountNumber == "2400" && !a.IsDeleted); if (!has2400) { _context.Set().Add(new Account { AccountNumber = "2400", Name = "Payroll Liabilities", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = false, IsActive = true, Description = "Payroll taxes and withholdings owed", CompanyId = company.Id, CreatedAt = now }); await _context.SaveChangesAsync(); added++; } // 2500 has always been resolved by number as the Gift Certificate Liability (GiftCertificatesController), // but the default-company seed created it as "Long-Term Loan" — so GC obligations were mislabeled there. // Rename it (only where it still carries the old default name) and mark it system. var legacyGcAcct = await _context.Set() .IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && a.AccountNumber == "2500" && !a.IsDeleted && a.Name == "Long-Term Loan"); if (legacyGcAcct != null) { legacyGcAcct.Name = "Gift Certificate Liability"; legacyGcAcct.Description = "Outstanding gift certificate obligations owed to certificate holders"; legacyGcAcct.IsSystem = true; legacyGcAcct.UpdatedAt = now; await _context.SaveChangesAsync(); } // 2500 Gift Certificate Liability — ensure it exists for companies that never got one (e.g. tenants // onboarded after the AccountingGapsPhase2 migration ran). Without it, GC GL postings silently no-op. var has2500 = await _context.Set() .IgnoreQueryFilters() .AnyAsync(a => a.CompanyId == company.Id && a.AccountNumber == "2500" && !a.IsDeleted); if (!has2500) { _context.Set().Add(new Account { AccountNumber = "2500", Name = "Gift Certificate Liability", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsSystem = true, IsActive = true, Description = "Outstanding gift certificate obligations owed to certificate holders", CompanyId = company.Id, CreatedAt = now }); await _context.SaveChangesAsync(); added++; } return added; } }