Demo data realism + invoice resend via SMS on any status
Seed data fixes: - Fix EF interceptor: no longer overwrites explicitly-set CreatedAt on Added entities — root cause of all "same month" chart issues - Customer seeder: generates 15 customers/month from Jan → current month; keeps 10 commercial anchors in deterministic order for job seeder index map - Invoice seeder: historical range bumped from 2→8 paid invoices/month so P&L shows consistent profit (~$5,200 collected vs ~$4,200 monthly expenses) - Month -1 bumped to 7 paid invoices to stay above expenses - Jobs: set UpdatedAt to historical event date so analytics don't need null fallback - Analytics (ReportsController): use CompletedDate ?? UpdatedAt ?? CreatedAt for revenue chart grouping; fixes empty Revenue Trend charts on Overview/Revenue tabs - SeedDataService: inject IAccountBalanceService; auto-recalculate account balances after seeding; patch checking/savings opening balances unconditionally on reset - Customer list: sort by CompanyName ?? ContactLastName so individuals and commercial accounts interleave instead of appearing as two blocks Invoice resend: - ResendInvoice action now accepts sendEmail + sendSms parameters; SMS-only resend no longer requires an email address on file - Ensures PublicViewToken exists before SMS so the view link is always valid - canResend in Details view now allows Paid invoices (removed != Paid guard) - Resend button shows channel-choice modal when customer has both email + SMS, direct SMS button when SMS only, or email button when email only - New #resendChannelModal mirrors the Send channel modal but posts to ResendInvoice - resendInvoice() JS updated to pass sendEmail/sendSms query params Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Application.Interfaces;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
using PowderCoating.Shared.Constants;
|
||||
|
||||
@@ -13,15 +14,18 @@ public partial class SeedDataService : ISeedDataService
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly RoleManager<IdentityRole> _roleManager;
|
||||
private readonly IAccountBalanceService _accountBalanceService;
|
||||
|
||||
public SeedDataService(
|
||||
ApplicationDbContext context,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RoleManager<IdentityRole> roleManager)
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
IAccountBalanceService accountBalanceService)
|
||||
{
|
||||
_context = context;
|
||||
_userManager = userManager;
|
||||
_roleManager = roleManager;
|
||||
_accountBalanceService = accountBalanceService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -426,8 +430,53 @@ public partial class SeedDataService : ISeedDataService
|
||||
await RunSeeder("Inv. txns", details, errors, result, () => SeedInventoryTransactionsAsync(company));
|
||||
await RunSeeder("Invoices", details, errors, result, () => SeedInvoicesAsync(company));
|
||||
await RunSeeder("AI predictions", details, errors, result, () => SeedAiPredictionsAsync(company));
|
||||
// Ensure chart of accounts exists before bills/expenses — both seeders silently return 0
|
||||
// if the AP or checking account is missing. SeedDefaultChartOfAccountsAsync is idempotent.
|
||||
try
|
||||
{
|
||||
var accountsAdded = await SeedDefaultChartOfAccountsAsync(company);
|
||||
var systemAccountsAdded = await EnsureSystemAccountsAsync(company);
|
||||
if (accountsAdded > 0)
|
||||
details.Add($"✓ {accountsAdded} chart of account(s) created");
|
||||
if (systemAccountsAdded > 0)
|
||||
details.Add($"✓ {systemAccountsAdded} missing system account(s) added");
|
||||
}
|
||||
catch (Exception ex) { errors.Add($"✗ Chart of accounts: {ex.Message}"); _context.ChangeTracker.Clear(); }
|
||||
|
||||
await RunSeeder("Vendor bills", details, errors, result, () => SeedBillsAsync(company));
|
||||
await RunSeeder("Expenses", details, errors, result, () => SeedExpensesAsync(company));
|
||||
|
||||
// Accounts survive resets (no removal sweep), so the chart-of-accounts seeder skips them
|
||||
// on every reset after the first. But 12 months of seeded expenses outpace ~3 months of
|
||||
// seeded revenue, and without a prior-period cash balance the checking account shows a
|
||||
// large negative. Patch the opening balances unconditionally so every reset is realistic.
|
||||
try
|
||||
{
|
||||
var checkingAcct = await _context.Set<Account>().IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted
|
||||
&& a.AccountSubType == AccountSubType.Checking);
|
||||
if (checkingAcct != null && checkingAcct.OpeningBalance == 0)
|
||||
{
|
||||
checkingAcct.OpeningBalance = 75_000m;
|
||||
checkingAcct.OpeningBalanceDate = DateTime.UtcNow.AddYears(-1);
|
||||
checkingAcct.CurrentBalance = 75_000m;
|
||||
await _context.SaveChangesAsync();
|
||||
details.Add("✓ Checking account opening balance set to $75,000");
|
||||
}
|
||||
var savingsAcct = await _context.Set<Account>().IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(a => a.CompanyId == company.Id && !a.IsDeleted
|
||||
&& a.AccountSubType == AccountSubType.Savings);
|
||||
if (savingsAcct != null && savingsAcct.OpeningBalance == 0)
|
||||
{
|
||||
savingsAcct.OpeningBalance = 14_500m;
|
||||
savingsAcct.OpeningBalanceDate = DateTime.UtcNow.AddYears(-1);
|
||||
savingsAcct.CurrentBalance = 14_500m;
|
||||
await _context.SaveChangesAsync();
|
||||
details.Add("✓ Savings account opening balance set to $14,500");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { errors.Add($"✗ Account opening balances: {ex.Message}"); _context.ChangeTracker.Clear(); }
|
||||
|
||||
await RunSeeder("Appointments", details, errors, result, () => SeedAppointmentsAsync(company));
|
||||
|
||||
if (company.CompanyCode == "DEMO")
|
||||
@@ -443,6 +492,15 @@ public partial class SeedDataService : ISeedDataService
|
||||
catch (Exception ex) { errors.Add($"✗ Demo users: {ex.Message}"); _context.ChangeTracker.Clear(); }
|
||||
}
|
||||
|
||||
// Replay all GL transactions so CurrentBalance reflects the full seeded history,
|
||||
// including the opening balances patched above.
|
||||
try
|
||||
{
|
||||
await _accountBalanceService.RecalculateAllAsync(company.Id);
|
||||
details.Add("✓ Account balances recalculated");
|
||||
}
|
||||
catch (Exception ex) { errors.Add($"✗ Account balance recalculation: {ex.Message}"); _context.ChangeTracker.Clear(); }
|
||||
|
||||
if (errors.Any())
|
||||
{
|
||||
details.AddRange(errors);
|
||||
|
||||
Reference in New Issue
Block a user