Initial commit
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user