using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// /// Seeds 26 invoices spanning three months of billing history: six paid in month −3, /// eight paid or partially paid in month −2, nine in month −1, and three recent plus /// one overdue in the current month. Each invoice is linked to a seeded job and /// has one or two line items plus an optional record. /// /// /// /// Idempotency: returns 0 immediately if any non-deleted invoices already exist for this company. /// /// /// The method resolves four accounting accounts by number/sub-type from the previously /// seeded chart of accounts (4000 Powder Coating Revenue, 4100 Sandblasting Revenue, /// 1000 Checking, 2200 Sales Tax Payable). If any account is not found its FK is simply /// omitted (null) rather than aborting the seed run. /// /// /// The private Inv() local async function handles building, saving, and optionally /// recording a payment for a single invoice. It is a local function rather than a separate /// method because it captures the outer variables jobs, seq, ji, /// and the four account references by closure. /// /// /// Payment dates follow the production convention: for fully-paid invoices the payment is /// recorded five days before the due date; for partially-paid invoices the deposit is /// recorded four days after the invoice date. /// /// /// The "overdue" demo invoice is created 35 days ago with Net-14 terms, placing it 21 days /// past due — enough to appear prominently in the AR Aging report. /// /// /// The tenant company to seed invoices for. /// Number of invoices inserted, or 0 if already seeded or no eligible jobs exist. private async Task SeedInvoicesAsync(Company company) { var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(i => i.CompanyId == company.Id && !i.IsDeleted); if (existingCount > 0) return 0; // ── Dependencies ────────────────────────────────────────────────────── var jobs = await _context.Set() .IgnoreQueryFilters() .Include(j => j.JobItems) .Where(j => j.CompanyId == company.Id && !j.IsDeleted && j.FinalPrice > 0 && j.CustomerId > 0) .OrderBy(j => j.Id) .ToListAsync(); if (jobs.Count == 0) return 0; var revenueAcct = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && a.AccountNumber == "4000" && !a.IsDeleted); var sandblastAcct = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && a.AccountNumber == "4100" && !a.IsDeleted); var checkingAcct = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && a.AccountSubType == AccountSubType.Checking && !a.IsDeleted); var salesTaxAcct = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(a => a.CompanyId == company.Id && a.AccountNumber == "2200" && !a.IsDeleted); var preparedBy = await _userManager.Users .Where(u => u.CompanyId == company.Id).FirstOrDefaultAsync(); var now = DateTime.UtcNow; var pfx = $"INV-{now:yy}{now.Month:D2}-"; var seq = 1; var seeded = 0; var ji = 0; // rotating job index // Rotates through the available jobs in order. Using modulo wrapping means the // seeder never throws even if there are fewer jobs than invoices to create. Job NextJob() { var j = jobs[ji % jobs.Count]; ji++; return j; } // ── Core builder ────────────────────────────────────────────────────── // // Builds and persists one Invoice + optional Payment in a single call. // Each invoice is saved immediately (not batched) so EF generates an ID before // the Payment FK is set. Tax is only added as a line item when taxPct > 0. async Task Inv( InvoiceStatus status, int daysAgo, // invoice creation date int dueDays, // net terms in days decimal taxPct, string terms, string? notes, // payment fields — ignored when status is Draft or Sent PaymentMethod pMethod = PaymentMethod.BankTransferACH, string? pRef = null) { var job = NextJob(); var date = now.AddDays(-daysAgo); var sub = job.FinalPrice; var taxAmt = Math.Round(sub * taxPct / 100m, 2); var total = sub + taxAmt; var isPaid = status == InvoiceStatus.Paid; var isPart = status == InvoiceStatus.PartiallyPaid; var amtPaid = isPaid ? total : isPart ? Math.Round(total * 0.5m, 2) : 0m; var desc = job.JobItems?.FirstOrDefault()?.Description ?? "Powder Coating Services"; var inv = new Invoice { InvoiceNumber = $"{pfx}{seq++:D4}", JobId = job.Id, CustomerId = job.CustomerId, PreparedById = preparedBy?.Id, Status = status, InvoiceDate = date, DueDate = date.AddDays(dueDays), SentDate = status != InvoiceStatus.Draft ? date : null, PaidDate = isPaid ? date.AddDays(dueDays - 5) : null, SubTotal = sub, TaxPercent = taxPct, TaxAmount = taxAmt, Total = total, AmountPaid = amtPaid, Terms = terms, Notes = notes, CustomerPO = job.CustomerPO, SalesTaxAccountId = taxAmt > 0 ? salesTaxAcct?.Id : null, CompanyId = company.Id, CreatedAt = date }; inv.InvoiceItems.Add(new InvoiceItem { Description = desc, Quantity = 1, UnitPrice = sub, TotalPrice = sub, RevenueAccountId = revenueAcct?.Id, DisplayOrder = 1, CompanyId = company.Id, CreatedAt = date }); if (taxAmt > 0) inv.InvoiceItems.Add(new InvoiceItem { Description = $"Sales Tax ({taxPct:0.##}%)", Quantity = 1, UnitPrice = taxAmt, TotalPrice = taxAmt, RevenueAccountId = salesTaxAcct?.Id, DisplayOrder = 2, CompanyId = company.Id, CreatedAt = date }); await _context.Set().AddAsync(inv); await _context.SaveChangesAsync(); seeded++; if (amtPaid > 0) { // Paid: payment date = dueDate - 5d. Partial: a few days after invoice var pDate = isPaid ? date.AddDays(dueDays - 5) : date.AddDays(4); await _context.Set().AddAsync(new Payment { InvoiceId = inv.Id, Amount = amtPaid, PaymentDate = pDate, PaymentMethod = pMethod, Reference = pRef, Notes = isPaid ? "Paid in full" : "50% deposit", RecordedById = preparedBy?.Id, DepositAccountId = checkingAcct?.Id, CompanyId = company.Id, CreatedAt = pDate }); await _context.SaveChangesAsync(); } } // ── Month −3 (6 paid) ───────────────────────────────────────────────── await Inv(InvoiceStatus.Paid, 88, 30, 0m, "Net 30", "Thank you for your business!", PaymentMethod.BankTransferACH); await Inv(InvoiceStatus.Paid, 84, 30, 7.5m, "Net 30", "Thank you for your business!", PaymentMethod.Check, "CHK-1041"); await Inv(InvoiceStatus.Paid, 80, 14, 0m, "Net 14", "Thank you!", PaymentMethod.Cash); await Inv(InvoiceStatus.Paid, 76, 30, 7.5m, "Net 30", "Thank you for your business!", PaymentMethod.BankTransferACH); await Inv(InvoiceStatus.Paid, 72, 30, 0m, "Net 30", "Thank you!", PaymentMethod.CreditDebitCard); await Inv(InvoiceStatus.Paid, 68, 30, 7.5m, "Net 30", "Thank you for your business!", PaymentMethod.Check, "CHK-1044"); // ── Month −2 (6 paid + 2 partial) ──────────────────────────────────── await Inv(InvoiceStatus.Paid, 62, 30, 0m, "Net 30", "Thank you!", PaymentMethod.BankTransferACH); await Inv(InvoiceStatus.Paid, 58, 30, 7.5m, "Net 30", "Thank you for your business!", PaymentMethod.Check, "CHK-1048"); await Inv(InvoiceStatus.Paid, 55, 14, 0m, "Due on receipt", "Paid on completion.", PaymentMethod.Cash); await Inv(InvoiceStatus.Paid, 51, 30, 7.5m, "Net 30", "Thank you!", PaymentMethod.BankTransferACH); await Inv(InvoiceStatus.Paid, 47, 30, 0m, "Net 30", "Thank you for your business!", PaymentMethod.CreditDebitCard); await Inv(InvoiceStatus.Paid, 43, 30, 7.5m, "Net 30", "Thank you!", PaymentMethod.Check, "CHK-1052"); await Inv(InvoiceStatus.PartiallyPaid, 40, 30, 0m, "Net 30", "50% deposit received — balance due.", PaymentMethod.Check, "CHK-1053"); await Inv(InvoiceStatus.PartiallyPaid, 37, 30, 7.5m, "Net 30", "Deposit on file — balance due on pickup.", PaymentMethod.BankTransferACH); // ── Month −1 (5 paid + 2 partial + 2 sent) ─────────────────────────── await Inv(InvoiceStatus.Paid, 32, 30, 0m, "Net 30", "Thank you!", PaymentMethod.BankTransferACH); await Inv(InvoiceStatus.Paid, 28, 30, 7.5m, "Net 30", "Thank you for your business!", PaymentMethod.Check, "CHK-1056"); await Inv(InvoiceStatus.Paid, 24, 14, 0m, "Net 14", "Thank you!", PaymentMethod.Cash); await Inv(InvoiceStatus.Paid, 20, 30, 7.5m, "Net 30", "Thank you for your business!", PaymentMethod.BankTransferACH); await Inv(InvoiceStatus.Paid, 16, 30, 0m, "Net 30", "Thank you!", PaymentMethod.CreditDebitCard); await Inv(InvoiceStatus.PartiallyPaid, 14, 30, 7.5m, "Net 30", "50% deposit received.", PaymentMethod.Check, "CHK-1060"); await Inv(InvoiceStatus.PartiallyPaid, 11, 30, 0m, "Net 30", "50% deposit — balance due on completion.", PaymentMethod.BankTransferACH); await Inv(InvoiceStatus.Sent, 9, 30, 7.5m, "Net 30", "Payment due within 30 days."); await Inv(InvoiceStatus.Sent, 6, 30, 0m, "Net 30", "Payment due within 30 days."); // ── Current month (1 overdue + 2 sent + 1 draft) ───────────────────── // Overdue: created 35 days ago on Net 14 terms → 21 days past due await Inv(InvoiceStatus.Sent, 35, 14, 7.5m, "Net 14", "PAST DUE — please remit payment immediately."); await Inv(InvoiceStatus.Sent, 4, 30, 0m, "Net 30", "Payment due within 30 days."); await Inv(InvoiceStatus.Sent, 2, 30, 7.5m, "Net 30", "Payment due within 30 days."); await Inv(InvoiceStatus.Draft, 1, 30, 0m, "Net 30", null); return seeded; } }