using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// /// Seeds a realistic 3-month history of vendor bills (Accounts Payable) across four /// spending categories — powder orders, consumables/hardware, equipment repairs, and /// utilities — with a mix of paid, partially-paid, and open statuses. /// /// /// The seeder constructs a narrative AP ledger covering approximately 90 days so that /// the AI Cash Flow Forecast, AR Aging, and Anomaly Detection reports have meaningful /// data to analyse from day one of a demo. /// /// Prerequisites: The method resolves all required accounts by /// or account number from the company's chart of accounts. /// It also resolves vendors by partial name match (e.g., "Prismatic", "Columbia"). /// If the AP account, the checking account, or at least one vendor is missing the method /// returns 0 (skips) rather than throwing, because those dependencies are seeded in /// earlier steps which may have failed. /// /// Local helper AddBill: Assigns the sequential bill number /// (format BILL-YYMM-####), stamps CompanyId on the bill and all its /// line items, saves, then optionally creates a with a /// sequential payment number (BPMT-YYMM-####). Separating save-per-bill from /// the payment save preserves the FK relationship without needing explicit navigation /// property loading. /// /// Double-entry note: The bill entity itself records the AP credit (via /// APAccountId); individual line items carry the debit account (e.g., powder /// expense account 5100). The payment records the AP debit and cash credit (via /// BankAccountId). The seed data mirrors this pattern but does NOT create /// journal entry rows — those are handled by the AP payment workflow in production. /// /// Status timeline: /// - Months −3 to −1: bills with matching paid payments (closed AP). /// - Current month: open bills (due soon) to populate the AP Aging report. /// - One overdue specialty bill (past due date, still open) to trigger the anomaly /// detection feature's "overdue payable" flag. /// /// Idempotency: returns 0 immediately if any bills exist for the company. /// /// The tenant company to seed bills for. /// The total number of records created. private async Task SeedBillsAsync(Company company) { var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(b => b.CompanyId == company.Id && !b.IsDeleted); if (existingCount > 0) return 0; // ── Account lookups ─────────────────────────────────────────────────── var apAccount = await _context.Set() .IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountSubType == AccountSubType.AccountsPayable); var checkingAccount = await _context.Set() .IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountSubType == AccountSubType.Checking); var powderAccount = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "5100"); var consumablesAccount = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "5200"); var equipRepairsAccount = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "6100"); var utilitiesAccount = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "6600"); if (apAccount == null || checkingAccount == null) return 0; // ── Vendor lookups ──────────────────────────────────────────────────── var vendors = await _context.Set() .IgnoreQueryFilters() .Where(v => v.CompanyId == company.Id && !v.IsDeleted) .ToListAsync(); var prismatic = vendors.FirstOrDefault(v => v.CompanyName.Contains("Prismatic")) ?? vendors.FirstOrDefault(); var columbia = vendors.FirstOrDefault(v => v.CompanyName.Contains("Columbia")) ?? vendors.FirstOrDefault(); var aceHardware = vendors.FirstOrDefault(v => v.CompanyName.Contains("Ace")) ?? vendors.FirstOrDefault(); var fastenal = vendors.FirstOrDefault(v => v.CompanyName.Contains("Fastenal")) ?? vendors.FirstOrDefault(); var fallback = vendors.FirstOrDefault(); if (fallback == null) return 0; var now = DateTime.UtcNow; var prefix = $"BILL-{now:yy}{now.Month:D2}-"; var pmtPfx = $"BPMT-{now:yy}{now.Month:D2}-"; var seeded = 0; var billSeq = 1; var pmtSeq = 1; // Helper: add a bill and optional payment, save, return bill async Task AddBill( Bill bill, BillPayment? payment = null) { bill.BillNumber = $"{prefix}{billSeq++:D4}"; bill.CompanyId = company.Id; foreach (var li in bill.LineItems) { li.CompanyId = company.Id; li.CreatedAt = bill.CreatedAt; } await _context.Set().AddAsync(bill); await _context.SaveChangesAsync(); seeded++; if (payment != null) { payment.PaymentNumber = $"{pmtPfx}{pmtSeq++:D4}"; payment.BillId = bill.Id; payment.CompanyId = company.Id; payment.CreatedAt = payment.PaymentDate; await _context.Set().AddAsync(payment); await _context.SaveChangesAsync(); } return bill; } // ── POWDER ORDERS ───────────────────────────────────────────────────── // Month -3: Large powder restock — Paid await AddBill(new Bill { VendorInvoiceNumber = "PP-77211", VendorId = (prismatic ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-90), DueDate = now.AddDays(-60), Status = BillStatus.Paid, Terms = "Net 30", Memo = "Quarterly powder restock — month 1", SubTotal = 1_145.00m, Total = 1_145.00m, AmountPaid = 1_145.00m, CreatedAt = now.AddDays(-90), LineItems = { new BillLineItem { AccountId = powderAccount?.Id, Description = "Matte Black Powder — 50 lbs", Quantity = 2, UnitPrice = 178.00m, Amount = 356.00m, DisplayOrder = 1 }, new BillLineItem { AccountId = powderAccount?.Id, Description = "Gloss White Powder — 50 lbs", Quantity = 2, UnitPrice = 165.00m, Amount = 330.00m, DisplayOrder = 2 }, new BillLineItem { AccountId = powderAccount?.Id, Description = "Satin Silver Powder — 25 lbs", Quantity = 2, UnitPrice = 144.50m, Amount = 289.00m, DisplayOrder = 3 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Masking Tape & Plugs Kit", Quantity = 1, UnitPrice = 170.00m, Amount = 170.00m, DisplayOrder = 4 } } }, new BillPayment { VendorId = (prismatic ?? fallback).Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-60), Amount = 1_145.00m, PaymentMethod = PaymentMethod.BankTransferACH, Memo = "PP-77211 — paid in full" }); // Month -2: Mid-cycle specialty colors — Paid await AddBill(new Bill { VendorInvoiceNumber = "CC-4401", VendorId = (columbia ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-65), DueDate = now.AddDays(-35), Status = BillStatus.Paid, Terms = "Net 30", Memo = "Specialty metallic & candy colors", SubTotal = 986.00m, Total = 986.00m, AmountPaid = 986.00m, CreatedAt = now.AddDays(-65), LineItems = { new BillLineItem { AccountId = powderAccount?.Id, Description = "Candy Red Metallic — 10 lbs", Quantity = 3, UnitPrice = 145.00m, Amount = 435.00m, DisplayOrder = 1 }, new BillLineItem { AccountId = powderAccount?.Id, Description = "Chrome Effect Powder — 10 lbs", Quantity = 2, UnitPrice = 168.00m, Amount = 336.00m, DisplayOrder = 2 }, new BillLineItem { AccountId = powderAccount?.Id, Description = "Hammertone Bronze — 10 lbs", Quantity = 1, UnitPrice = 150.50m, Amount = 150.50m, DisplayOrder = 3 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Ground Straps & Hooks", Quantity = 1, UnitPrice = 64.50m, Amount = 64.50m, DisplayOrder = 4 } } }, new BillPayment { VendorId = (columbia ?? fallback).Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-35), Amount = 986.00m, PaymentMethod = PaymentMethod.BankTransferACH, Memo = "CC-4401 — paid in full" }); // Month -1: Regular restock — Paid await AddBill(new Bill { VendorInvoiceNumber = "PP-83440", VendorId = (prismatic ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-45), DueDate = now.AddDays(-15), Status = BillStatus.Paid, Terms = "Net 30", Memo = "Monthly powder restock", SubTotal = 892.50m, Total = 892.50m, AmountPaid = 892.50m, CreatedAt = now.AddDays(-45), LineItems = { new BillLineItem { AccountId = powderAccount?.Id, Description = "Matte Black Powder — 25 lbs", Quantity = 5, UnitPrice = 89.00m, Amount = 445.00m, DisplayOrder = 1 }, new BillLineItem { AccountId = powderAccount?.Id, Description = "Gloss White Powder — 25 lbs", Quantity = 4, UnitPrice = 86.50m, Amount = 346.00m, DisplayOrder = 2 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Masking Plugs Assortment", Quantity = 1, UnitPrice = 101.50m, Amount = 101.50m, DisplayOrder = 3 } } }, new BillPayment { VendorId = (prismatic ?? fallback).Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-15), Amount = 892.50m, PaymentMethod = PaymentMethod.BankTransferACH, Memo = "PP-83440 — paid in full" }); // Current month: Open (due soon) await AddBill(new Bill { VendorInvoiceNumber = "PP-88530", VendorId = (prismatic ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-8), DueDate = now.AddDays(22), Status = BillStatus.Open, Terms = "Net 30", Memo = "Current month powder order", SubTotal = 1_050.00m, Total = 1_050.00m, AmountPaid = 0m, CreatedAt = now.AddDays(-8), LineItems = { new BillLineItem { AccountId = powderAccount?.Id, Description = "Matte Black Powder — 25 lbs", Quantity = 6, UnitPrice = 89.00m, Amount = 534.00m, DisplayOrder = 1 }, new BillLineItem { AccountId = powderAccount?.Id, Description = "Gloss Red Powder — 10 lbs", Quantity = 2, UnitPrice = 132.00m, Amount = 264.00m, DisplayOrder = 2 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Hanging Racks (10-pack)", Quantity = 2, UnitPrice = 126.00m, Amount = 252.00m, DisplayOrder = 3 } } }); // Overdue specialty order await AddBill(new Bill { VendorInvoiceNumber = "CC-5509", VendorId = (columbia ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-50), DueDate = now.AddDays(-5), Status = BillStatus.Open, Terms = "Net 45", Memo = "Specialty colors — overdue", SubTotal = 1_240.00m, Total = 1_240.00m, AmountPaid = 0m, CreatedAt = now.AddDays(-50), LineItems = { new BillLineItem { AccountId = powderAccount?.Id, Description = "Candy Red Metallic — 10 lbs", Quantity = 3, UnitPrice = 145.00m, Amount = 435.00m, DisplayOrder = 1 }, new BillLineItem { AccountId = powderAccount?.Id, Description = "Chrome Effect — 10 lbs", Quantity = 3, UnitPrice = 168.00m, Amount = 504.00m, DisplayOrder = 2 }, new BillLineItem { AccountId = powderAccount?.Id, Description = "Hammertone Bronze — 10 lbs", Quantity = 2, UnitPrice = 150.50m, Amount = 301.00m, DisplayOrder = 3 } } }); // ── CONSUMABLES / HARDWARE ───────────────────────────────────────────── // Month -3: Fastenal hardware — Paid await AddBill(new Bill { VendorInvoiceNumber = "FST-18822", VendorId = (fastenal ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-85), DueDate = now.AddDays(-55), Status = BillStatus.Paid, Terms = "Net 30", Memo = "Hardware & consumables restock", SubTotal = 412.50m, Total = 412.50m, AmountPaid = 412.50m, CreatedAt = now.AddDays(-85), LineItems = { new BillLineItem { AccountId = consumablesAccount?.Id, Description = "J-Hook Hangers Assortment", Quantity = 2, UnitPrice = 89.75m, Amount = 179.50m, DisplayOrder = 1 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Masking Caps — Mixed (100-pack)", Quantity = 2, UnitPrice = 60.00m, Amount = 120.00m, DisplayOrder = 2 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Wire Brushes & Abrasives", Quantity = 1, UnitPrice = 113.00m, Amount = 113.00m, DisplayOrder = 3 } } }, new BillPayment { VendorId = (fastenal ?? fallback).Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-55), Amount = 412.50m, PaymentMethod = PaymentMethod.Check, CheckNumber = "1082", Memo = "FST-18822 — paid in full" }); // Month -1: Fastenal — Paid await AddBill(new Bill { VendorInvoiceNumber = "FST-20041", VendorId = (fastenal ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-40), DueDate = now.AddDays(-10), Status = BillStatus.Paid, Terms = "Net 30", Memo = "Monthly hardware restock", SubTotal = 298.00m, Total = 298.00m, AmountPaid = 298.00m, CreatedAt = now.AddDays(-40), LineItems = { new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Sandpaper & Abrasive Pads (assorted)", Quantity = 2, UnitPrice = 74.50m, Amount = 149.00m, DisplayOrder = 1 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Masking Caps — Small (100-pack)", Quantity = 2, UnitPrice = 60.00m, Amount = 120.00m, DisplayOrder = 2 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Safety Gloves (12-pair pack)", Quantity = 1, UnitPrice = 29.00m, Amount = 29.00m, DisplayOrder = 3 } } }, new BillPayment { VendorId = (fastenal ?? fallback).Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-10), Amount = 298.00m, PaymentMethod = PaymentMethod.Check, CheckNumber = "1086", Memo = "FST-20041 — paid in full" }); // Current: Fastenal — Open await AddBill(new Bill { VendorInvoiceNumber = "FST-20441", VendorId = (fastenal ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-7), DueDate = now.AddDays(23), Status = BillStatus.Open, Terms = "Net 30", Memo = "Monthly hardware & fastener restock", SubTotal = 318.75m, Total = 318.75m, AmountPaid = 0m, CreatedAt = now.AddDays(-7), LineItems = { new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Hanging Racks & J-Hooks", Quantity = 1, UnitPrice = 198.75m, Amount = 198.75m, DisplayOrder = 1 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Masking Caps — Mixed (100-pack)", Quantity = 2, UnitPrice = 60.00m, Amount = 120.00m, DisplayOrder = 2 } } }); // ── EQUIPMENT REPAIRS ───────────────────────────────────────────────── // Month -3: Sandblaster service — Paid await AddBill(new Bill { VendorInvoiceNumber = "ACE-6901", VendorId = (aceHardware ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-80), DueDate = now.AddDays(-50), Status = BillStatus.Paid, Terms = "Net 30", Memo = "Sandblaster nozzle & cabinet seal replacement", SubTotal = 310.00m, Total = 310.00m, AmountPaid = 310.00m, CreatedAt = now.AddDays(-80), LineItems = { new BillLineItem { AccountId = equipRepairsAccount?.Id, Description = "Blast Nozzle Tungsten — 3/8\"", Quantity = 2, UnitPrice = 85.00m, Amount = 170.00m, DisplayOrder = 1 }, new BillLineItem { AccountId = equipRepairsAccount?.Id, Description = "Cabinet Door Seal Kit", Quantity = 1, UnitPrice = 140.00m, Amount = 140.00m, DisplayOrder = 2 } } }, new BillPayment { VendorId = (aceHardware ?? fallback).Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-50), Amount = 310.00m, PaymentMethod = PaymentMethod.Check, CheckNumber = "1079", Memo = "ACE-6901 — sandblaster parts" }); // Month -1: Oven repair — Partially paid await AddBill(new Bill { VendorInvoiceNumber = "ACE-7714", VendorId = (aceHardware ?? fallback).Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-20), DueDate = now.AddDays(10), Status = BillStatus.PartiallyPaid, Terms = "Net 30", Memo = "Oven heating element + shop supplies", SubTotal = 540.00m, Total = 540.00m, AmountPaid = 200.00m, CreatedAt = now.AddDays(-20), LineItems = { new BillLineItem { AccountId = equipRepairsAccount?.Id, Description = "Oven Heating Element — 240V 5000W", Quantity = 1, UnitPrice = 385.00m, Amount = 385.00m, DisplayOrder = 1 }, new BillLineItem { AccountId = consumablesAccount?.Id, Description = "Shop Supplies — Wire Brushes, Sandpaper", Quantity = 1, UnitPrice = 155.00m, Amount = 155.00m, DisplayOrder = 2 } } }, new BillPayment { VendorId = (aceHardware ?? fallback).Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-10), Amount = 200.00m, PaymentMethod = PaymentMethod.Check, CheckNumber = "1087", Memo = "ACE-7714 — partial payment" }); // ── UTILITIES (3 months each) ───────────────────────────────────────── // Electric — month -3 (paid) await AddBill(new Bill { VendorInvoiceNumber = "ELEC-2024-01", VendorId = fallback.Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-90), DueDate = now.AddDays(-75), Status = BillStatus.Paid, Terms = "Due on Receipt", Memo = "Electric — 3 months ago", SubTotal = 592.10m, Total = 592.10m, AmountPaid = 592.10m, CreatedAt = now.AddDays(-90), LineItems = { new BillLineItem { AccountId = utilitiesAccount?.Id, Description = "Commercial Electric — monthly usage", Quantity = 1, UnitPrice = 592.10m, Amount = 592.10m, DisplayOrder = 1 } } }, new BillPayment { VendorId = fallback.Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-75), Amount = 592.10m, PaymentMethod = PaymentMethod.BankTransferACH, Memo = "Electric bill — auto pay" }); // Electric — month -2 (paid) await AddBill(new Bill { VendorInvoiceNumber = "ELEC-2024-02", VendorId = fallback.Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-60), DueDate = now.AddDays(-45), Status = BillStatus.Paid, Terms = "Due on Receipt", Memo = "Electric — 2 months ago", SubTotal = 618.42m, Total = 618.42m, AmountPaid = 618.42m, CreatedAt = now.AddDays(-60), LineItems = { new BillLineItem { AccountId = utilitiesAccount?.Id, Description = "Commercial Electric — monthly usage", Quantity = 1, UnitPrice = 618.42m, Amount = 618.42m, DisplayOrder = 1 } } }, new BillPayment { VendorId = fallback.Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-45), Amount = 618.42m, PaymentMethod = PaymentMethod.BankTransferACH, Memo = "Electric bill — auto pay" }); // Electric — month -1 (paid) await AddBill(new Bill { VendorInvoiceNumber = "ELEC-2024-03", VendorId = fallback.Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-30), DueDate = now.AddDays(-15), Status = BillStatus.Paid, Terms = "Due on Receipt", Memo = "Electric — last month", SubTotal = 574.88m, Total = 574.88m, AmountPaid = 574.88m, CreatedAt = now.AddDays(-30), LineItems = { new BillLineItem { AccountId = utilitiesAccount?.Id, Description = "Commercial Electric — monthly usage", Quantity = 1, UnitPrice = 574.88m, Amount = 574.88m, DisplayOrder = 1 } } }, new BillPayment { VendorId = fallback.Id, BankAccountId = checkingAccount.Id, PaymentDate = now.AddDays(-15), Amount = 574.88m, PaymentMethod = PaymentMethod.BankTransferACH, Memo = "Electric bill — auto pay" }); // Electric — current month (open) await AddBill(new Bill { VendorInvoiceNumber = "ELEC-2024-04", VendorId = fallback.Id, APAccountId = apAccount.Id, BillDate = now.AddDays(-2), DueDate = now.AddDays(15), Status = BillStatus.Open, Terms = "Due on Receipt", Memo = "Electric — current month", SubTotal = 601.30m, Total = 601.30m, AmountPaid = 0m, CreatedAt = now.AddDays(-2), LineItems = { new BillLineItem { AccountId = utilitiesAccount?.Id, Description = "Commercial Electric — monthly usage", Quantity = 1, UnitPrice = 601.30m, Amount = 601.30m, DisplayOrder = 1 } } }); return seeded; } /// /// Seeds recurring direct-expense transactions for the company covering the past ~90 days: /// shop rent, natural gas, business insurance, Google Ads / Yelp, software subscriptions, /// office supplies, and monthly bank/card-processing fees. /// /// /// Expenses differ from bills in that they are single-line, immediately-paid transactions /// (no AP intermediary) — they debit an expense account and credit a cash or credit-card /// account directly. This mirrors the accounting pattern used by the Expenses module. /// /// Account resolution: Accounts are looked up by account number (e.g., "6500" for /// rent, "6600" for utilities) rather than by name so that customised display names do not /// break the seeder. A cascading fallback chain ensures the method still seeds something /// useful even if the chart of accounts was partially seeded — the first non-null expense /// account found is used as a last-resort category. If no expense account and no checking /// account are found, the method returns 0 and logs nothing (silent skip handled by caller). /// /// Credit card vs. checking: Advertising and software subscriptions are charged to /// the credit card account when one exists (matching real-world business practice). All /// other expenses use the checking account. If no credit card account was seeded, the /// checking account is used as fallback for all categories. /// /// Local helper AddExp: Assigns a sequential expense number /// (format EXP-YYMM-####), saves immediately per expense so that the sequence /// counter increments correctly and individual failures can be diagnosed from logs. /// /// Idempotency: returns 0 immediately if any expense records exist for the company. /// /// The tenant company to seed expenses for. /// The total number of records created. private async Task SeedExpensesAsync(Company company) { var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(e => e.CompanyId == company.Id && !e.IsDeleted); if (existingCount > 0) return 0; // ── Account lookups ─────────────────────────────────────────────────── var checkingAccount = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountSubType == AccountSubType.Checking); var creditCardAccount = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountSubType == AccountSubType.CreditCard); var utilitiesAcct = await _context.Set().IgnoreQueryFilters().FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "6600"); var rentAcct = await _context.Set().IgnoreQueryFilters().FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "6500"); var advertisingAcct = await _context.Set().IgnoreQueryFilters().FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "6000"); var officeSuppliesAcct = await _context.Set().IgnoreQueryFilters().FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "6800"); var bankChargesAcct = await _context.Set().IgnoreQueryFilters().FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "6900"); var insuranceAcct = await _context.Set().IgnoreQueryFilters().FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountNumber == "6200"); if (checkingAccount == null) return 0; var fallbackExpense = utilitiesAcct ?? rentAcct ?? advertisingAcct ?? await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted && a.AccountType == AccountType.Expense); if (fallbackExpense == null) return 0; var vendors = await _context.Set().IgnoreQueryFilters() .Where(v => v.CompanyId == company.Id && !v.IsDeleted).ToListAsync(); var now = DateTime.UtcNow; var pfx = $"EXP-{now:yy}{now.Month:D2}-"; var seeded = 0; var expSeq = 1; var cc = creditCardAccount ?? checkingAccount; async Task AddExp( DateTime date, int? vendorId, Account expAcct, Account pmtAcct, PaymentMethod method, decimal amount, string memo) { await _context.Set().AddAsync(new Expense { ExpenseNumber = $"{pfx}{expSeq++:D4}", Date = date, VendorId = vendorId, ExpenseAccountId = expAcct.Id, PaymentAccountId = pmtAcct.Id, PaymentMethod = method, Amount = amount, Memo = memo, CompanyId = company.Id, CreatedAt = date }); await _context.SaveChangesAsync(); seeded++; } // ── SHOP RENT — 3 months ────────────────────────────────────────────── var rentAccount = rentAcct ?? fallbackExpense; await AddExp(now.AddDays(-95), null, rentAccount, checkingAccount, PaymentMethod.Check, 2_400.00m, "Shop rent — 3 months ago"); await AddExp(now.AddDays(-65), null, rentAccount, checkingAccount, PaymentMethod.Check, 2_400.00m, "Shop rent — 2 months ago"); await AddExp(now.AddDays(-35), null, rentAccount, checkingAccount, PaymentMethod.Check, 2_400.00m, "Shop rent — last month"); await AddExp(now.AddDays(-3), null, rentAccount, checkingAccount, PaymentMethod.Check, 2_400.00m, "Shop rent — current month"); // ── NATURAL GAS — 3 months ──────────────────────────────────────────── var utilAccount = utilitiesAcct ?? fallbackExpense; await AddExp(now.AddDays(-88), null, utilAccount, checkingAccount, PaymentMethod.BankTransferACH, 218.44m, "Natural gas — 3 months ago"); await AddExp(now.AddDays(-58), null, utilAccount, checkingAccount, PaymentMethod.BankTransferACH, 241.60m, "Natural gas — 2 months ago"); await AddExp(now.AddDays(-28), null, utilAccount, checkingAccount, PaymentMethod.BankTransferACH, 196.30m, "Natural gas — last month"); // ── INSURANCE ───────────────────────────────────────────────────────── var insAccount = insuranceAcct ?? fallbackExpense; await AddExp(now.AddDays(-90), null, insAccount, checkingAccount, PaymentMethod.Check, 785.00m, "Business liability insurance — quarterly premium Q1"); await AddExp(now.AddDays(-1), null, insAccount, checkingAccount, PaymentMethod.Check, 785.00m, "Business liability insurance — quarterly premium Q2"); // ── MARKETING / ADVERTISING ─────────────────────────────────────────── var adAccount = advertisingAcct ?? fallbackExpense; await AddExp(now.AddDays(-80), null, adAccount, cc, PaymentMethod.CreditDebitCard, 150.00m, "Google Ads — local search campaign"); await AddExp(now.AddDays(-50), null, adAccount, cc, PaymentMethod.CreditDebitCard, 150.00m, "Google Ads — local search campaign"); await AddExp(now.AddDays(-20), null, adAccount, cc, PaymentMethod.CreditDebitCard, 175.00m, "Google Ads — expanded local campaign"); await AddExp(now.AddDays(-15), null, adAccount, cc, PaymentMethod.CreditDebitCard, 89.00m, "Yelp advertising — monthly"); await AddExp(now.AddDays(-5), null, adAccount, cc, PaymentMethod.CreditDebitCard, 175.00m, "Google Ads — current month"); // ── SOFTWARE SUBSCRIPTIONS ──────────────────────────────────────────── var swAccount = officeSuppliesAcct ?? fallbackExpense; await AddExp(now.AddDays(-90), null, swAccount, cc, PaymentMethod.CreditDebitCard, 29.00m, "QuickBooks subscription — monthly"); await AddExp(now.AddDays(-60), null, swAccount, cc, PaymentMethod.CreditDebitCard, 29.00m, "QuickBooks subscription — monthly"); await AddExp(now.AddDays(-30), null, swAccount, cc, PaymentMethod.CreditDebitCard, 29.00m, "QuickBooks subscription — monthly"); await AddExp(now.AddDays(-2), null, swAccount, cc, PaymentMethod.CreditDebitCard, 29.00m, "QuickBooks subscription — monthly"); // ── OFFICE SUPPLIES ─────────────────────────────────────────────────── var offAccount = officeSuppliesAcct ?? fallbackExpense; await AddExp(now.AddDays(-75), vendors.FirstOrDefault()?.Id, offAccount, cc, PaymentMethod.CreditDebitCard, 63.40m, "Office supplies — printer paper, labels, pens"); await AddExp(now.AddDays(-8), vendors.FirstOrDefault()?.Id, offAccount, cc, PaymentMethod.CreditDebitCard, 47.83m, "Office supplies — printer paper, pens, labels"); // ── BANK FEES ───────────────────────────────────────────────────────── var bankAccount = bankChargesAcct ?? fallbackExpense; await AddExp(now.AddDays(-85), null, bankAccount, checkingAccount, PaymentMethod.BankTransferACH, 28.50m, "Monthly card processing fees"); await AddExp(now.AddDays(-55), null, bankAccount, checkingAccount, PaymentMethod.BankTransferACH, 31.20m, "Monthly card processing fees"); await AddExp(now.AddDays(-25), null, bankAccount, checkingAccount, PaymentMethod.BankTransferACH, 29.80m, "Monthly card processing fees"); await AddExp(now.AddDays(-3), null, bankAccount, checkingAccount, PaymentMethod.BankTransferACH, 32.15m, "Monthly card processing fees"); return seeded; } }