Fix subscription expiry logic and HTML entities in page titles

Subscription expiry (SubscriptionExpiryBackgroundService):
- Trials with no grace period now go directly Active -> Expired instead
  of briefly entering GracePeriod for a day, which was causing repeated
  'Grace Period Started' admin notification emails
- Remove redundant isTrial variable (query already filters to non-Stripe
  companies, so all processed companies are trials by definition)
- Save per-company inside the loop so a single SaveChangesAsync failure
  no longer discards all other companies' status changes and notification
  log entries (which was the other cause of repeated emails)

HTML entities in page titles (33 views):
- Replace – / — with plain ' - ' in ViewData["Title"] C#
  strings; Razor HTML-encodes these when rendering @ViewData["Title"],
  causing browsers to display the literal text '–' instead of a dash

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 09:43:41 -04:00
parent 19e1ce858f
commit e476b4744d
34 changed files with 131 additions and 94 deletions
@@ -1,8 +1,8 @@
@model PowderCoating.Application.DTOs.Accounting.AccountLedgerDto
@model PowderCoating.Application.DTOs.Accounting.AccountLedgerDto
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = $"Ledger &mdash; {Model.AccountNumber} {Model.Name}";
ViewData["Title"] = $"Ledger - {Model.AccountNumber} {Model.Name}";
ViewData["PageIcon"] = "bi-journal-text";
ViewData["PageHelpTitle"] = "Account Ledger";
ViewData["PageHelpContent"] = "A chronological list of every transaction posted to this account. Click any Reference to open the source record. Debit increases asset and expense accounts; credit increases liability, equity, and revenue accounts. Use the date range or quick buttons (This Month, YTD, etc.) to narrow the view.";
@@ -1,7 +1,7 @@
@model PowderCoating.Core.Entities.BankReconciliation
@model PowderCoating.Core.Entities.BankReconciliation
@using PowderCoating.Web.Controllers
@{
ViewData["Title"] = $"Reconciliation Report &ndash; {Model.Account?.Name}";
ViewData["Title"] = $"Reconciliation Report - {Model.Account?.Name}";
var clearedDeposits = ViewBag.ClearedDeposits as IEnumerable<PowderCoating.Core.Entities.Payment> ?? Enumerable.Empty<PowderCoating.Core.Entities.Payment>();
var clearedPayments = ViewBag.ClearedPayments as List<ReconciliationItem> ?? new();
}
@@ -1,8 +1,8 @@
@using PowderCoating.Web.Controllers
@using PowderCoating.Web.Controllers
@model BudgetCreateVm
@{
ViewData["Title"] = $"Edit Budget &mdash; {Model.Name}";
ViewData["Title"] = $"Edit Budget - {Model.Name}";
ViewData["PageIcon"] = "bi-pencil";
var months = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
}
@@ -1,6 +1,6 @@
@model PowderCoating.Application.DTOs.Notification.NotificationTemplateDto
@model PowderCoating.Application.DTOs.Notification.NotificationTemplateDto
@{
ViewData["Title"] = $"Edit Template &mdash; {Model.DisplayName}";
ViewData["Title"] = $"Edit Template - {Model.DisplayName}";
ViewData["PageIcon"] = "bi-envelope-gear";
var placeholders = ViewBag.Placeholders as List<(string Placeholder, string Description)>
?? new List<(string, string)>();
@@ -1,6 +1,6 @@
@model PowderCoating.Application.DTOs.Accounting.CustomerStatementDto
@model PowderCoating.Application.DTOs.Accounting.CustomerStatementDto
@{
ViewData["Title"] = $"Statement &ndash; {Model.CustomerName}";
ViewData["Title"] = $"Statement - {Model.CustomerName}";
}
<div class="d-flex justify-content-between align-items-start mb-4 flex-wrap gap-2">
@@ -1,5 +1,5 @@
@{
ViewData["Title"] = "Custom Formula Item Templates &mdash; Help";
@{
ViewData["Title"] = "Custom Formula Item Templates - Help";
}
<div class="row g-4">
@@ -1,6 +1,6 @@
@model PowderCoating.Application.DTOs.Inventory.InventoryItemDto
@model PowderCoating.Application.DTOs.Inventory.InventoryItemDto
@{
ViewData["Title"] = $"Label &mdash; {Model.Name}";
ViewData["Title"] = $"Label - {Model.Name}";
Layout = null; // standalone print page
}
<!DOCTYPE html>
@@ -1,4 +1,4 @@
@using PowderCoating.Application.DTOs.Inventory
@using PowderCoating.Application.DTOs.Inventory
@using PowderCoating.Web.Controllers
@{
var item = ViewBag.ItemDto as InventoryItemDto;
@@ -6,7 +6,7 @@
var otherJobs = ViewBag.OtherJobs as List<ScanJobOption> ?? new();
var preselectedJobId = ViewBag.PreselectedJobId as int?;
var scanError = ViewBag.ScanError as string;
ViewData["Title"] = $"Log Usage &mdash; {item?.Name}";
ViewData["Title"] = $"Log Usage - {item?.Name}";
Layout = null; // mobile-first standalone page
}
<!DOCTYPE html>
@@ -1,8 +1,8 @@
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
@model PowderCoating.Application.DTOs.Job.JobEditItemsViewModel
@using PowderCoating.Core.Entities
@{
ViewData["Title"] = $"Edit Items &mdash; {Model.JobNumber}";
ViewData["Title"] = $"Edit Items - {Model.JobNumber}";
ViewData["PageIcon"] = "bi-list-check";
}
@@ -1,7 +1,7 @@
@model (PowderCoating.Application.DTOs.Job.JobDto Job, PowderCoating.Application.DTOs.Job.IntakeJobDto Form)
@model (PowderCoating.Application.DTOs.Job.JobDto Job, PowderCoating.Application.DTOs.Job.IntakeJobDto Form)
@{
ViewData["Title"] = $"Part Intake &mdash; {Model.Job.JobNumber}";
ViewData["Title"] = $"Part Intake - {Model.Job.JobNumber}";
ViewData["PageIcon"] = "bi-box-seam";
var job = Model.Job;
var form = Model.Form;
@@ -1,8 +1,8 @@
@using PowderCoating.Application.DTOs.PurchaseOrder
@using PowderCoating.Application.DTOs.PurchaseOrder
@model ReceivePurchaseOrderDto
@{
ViewData["Title"] = $"Receive Goods &mdash; {ViewBag.PoNumber}";
ViewData["Title"] = $"Receive Goods - {ViewBag.PoNumber}";
int poId = (int)ViewBag.PoId;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep1Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Company Profile";
ViewData["Title"] = "Setup Wizard - Company Profile";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 1;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep9Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Team Members";
ViewData["Title"] = "Setup Wizard - Team Members";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 10;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep6Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Chart of Accounts";
ViewData["Title"] = "Setup Wizard - Chart of Accounts";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 11;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep10Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Vendors & Suppliers";
ViewData["Title"] = "Setup Wizard - Vendors & Suppliers";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 12;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep8Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Inventory / Powder Colors";
ViewData["Title"] = "Setup Wizard - Inventory / Powder Colors";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 13;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardOvensStepDto
@{
ViewData["Title"] = "Setup Wizard &mdash; Equipment & Ovens";
ViewData["Title"] = "Setup Wizard - Equipment & Ovens";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 14;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardPricingTiersStepDto
@{
ViewData["Title"] = "Setup Wizard &mdash; Pricing Tiers";
ViewData["Title"] = "Setup Wizard - Pricing Tiers";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 15;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardCatalogStepDto
@{
ViewData["Title"] = "Setup Wizard &mdash; Service Catalog";
ViewData["Title"] = "Setup Wizard - Service Catalog";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 16;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep7Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Notifications";
ViewData["Title"] = "Setup Wizard - Notifications";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 17;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep9Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Team Members";
ViewData["Title"] = "Setup Wizard - Team Members";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 18;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep2QbDto
@{
ViewData["Title"] = "Setup Wizard &mdash; QuickBooks Migration";
ViewData["Title"] = "Setup Wizard - QuickBooks Migration";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 2;
}
@@ -1,8 +1,8 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Core.Enums
@model WizardStep2Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Operating Costs";
ViewData["Title"] = "Setup Wizard - Operating Costs";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 3;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardOvensStepDto
@{
ViewData["Title"] = "Setup Wizard &mdash; Shop Equipment";
ViewData["Title"] = "Setup Wizard - Shop Equipment";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 4;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep3Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Document Numbering";
ViewData["Title"] = "Setup Wizard - Document Numbering";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 5;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep5Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Job Settings";
ViewData["Title"] = "Setup Wizard - Job Settings";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 6;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep4Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Payment Terms";
ViewData["Title"] = "Setup Wizard - Payment Terms";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 7;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardPricingTiersStepDto
@{
ViewData["Title"] = "Setup Wizard &mdash; Pricing Tiers";
ViewData["Title"] = "Setup Wizard - Pricing Tiers";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 8;
}
@@ -1,7 +1,7 @@
@using PowderCoating.Application.DTOs.Wizard
@using PowderCoating.Application.DTOs.Wizard
@model WizardStep7Dto
@{
ViewData["Title"] = "Setup Wizard &mdash; Notifications";
ViewData["Title"] = "Setup Wizard - Notifications";
var progress = ViewBag.Progress as WizardProgressDto ?? new WizardProgressDto();
int step = ViewBag.Step as int? ?? 5;
}
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8" />
@@ -1,7 +1,7 @@
@using PowderCoating.Core.Entities
@using PowderCoating.Core.Entities
@model StripeWebhookEvent
@{
ViewData["Title"] = $"Webhook Event &ndash; {Model.EventId}";
ViewData["Title"] = $"Webhook Event - {Model.EventId}";
var statusClass = Model.Status switch
{
StripeWebhookEventStatus.Processed => "success",
@@ -2,7 +2,7 @@
@using PowderCoating.Core.Enums
@model Company
@{
ViewData["Title"] = $"Manage &ndash; {Model.CompanyName}";
ViewData["Title"] = $"Manage - {Model.CompanyName}";
var planConfigs = (dynamic)ViewBag.PlanConfigs;
string PlanName(int plan)
@@ -1,6 +1,6 @@
@model PowderCoating.Application.DTOs.Accounting.VendorStatementDto
@model PowderCoating.Application.DTOs.Accounting.VendorStatementDto
@{
ViewData["Title"] = $"Statement &ndash; {Model.VendorName}";
ViewData["Title"] = $"Statement - {Model.VendorName}";
}
<div class="d-flex justify-content-between align-items-start mb-4 flex-wrap gap-2">