225 lines
13 KiB
C#
225 lines
13 KiB
C#
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;
|
||
}
|
||
}
|