Files
PowderCoatingLogix/src/PowderCoating.Infrastructure/Services/SeedDataService.Invoices.cs
T
2026-04-23 21:38:24 -04:00

225 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
namespace PowderCoating.Infrastructure.Services;
public partial class SeedDataService
{
/// <summary>
/// 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 <see cref="Payment"/> record.
/// </summary>
/// <remarks>
/// <para>
/// Idempotency: returns 0 immediately if any non-deleted invoices already exist for this company.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// The private <c>Inv()</c> 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 <c>jobs</c>, <c>seq</c>, <c>ji</c>,
/// and the four account references by closure.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <param name="company">The tenant company to seed invoices for.</param>
/// <returns>Number of invoices inserted, or 0 if already seeded or no eligible jobs exist.</returns>
private async Task<int> SeedInvoicesAsync(Company company)
{
var existingCount = await _context.Set<Invoice>()
.IgnoreQueryFilters()
.CountAsync(i => i.CompanyId == company.Id && !i.IsDeleted);
if (existingCount > 0)
return 0;
// ── Dependencies ──────────────────────────────────────────────────────
var jobs = await _context.Set<Job>()
.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<Account>().IgnoreQueryFilters()
.FirstOrDefaultAsync(a => a.CompanyId == company.Id && a.AccountNumber == "4000" && !a.IsDeleted);
var sandblastAcct = await _context.Set<Account>().IgnoreQueryFilters()
.FirstOrDefaultAsync(a => a.CompanyId == company.Id && a.AccountNumber == "4100" && !a.IsDeleted);
var checkingAcct = await _context.Set<Account>().IgnoreQueryFilters()
.FirstOrDefaultAsync(a => a.CompanyId == company.Id
&& a.AccountSubType == AccountSubType.Checking && !a.IsDeleted);
var salesTaxAcct = await _context.Set<Account>().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<Invoice>().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<Payment>().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;
}
}