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