diff --git a/src/PowderCoating.Application/DTOs/Inventory/InventoryDtos.cs b/src/PowderCoating.Application/DTOs/Inventory/InventoryDtos.cs
index a2d59e2..846703d 100644
--- a/src/PowderCoating.Application/DTOs/Inventory/InventoryDtos.cs
+++ b/src/PowderCoating.Application/DTOs/Inventory/InventoryDtos.cs
@@ -258,6 +258,8 @@ public class PowderUsageLogDto
public decimal VarianceLbs { get; set; }
public DateTime RecordedAt { get; set; }
public string? Notes { get; set; }
+ /// Set only for rows synthesized from a scan-based InventoryTransaction (no PowderUsageLog record).
+ public int? SourceTransactionId { get; set; }
}
public class InventoryLedgerViewModel
diff --git a/src/PowderCoating.Application/DTOs/Job/JobDtos.cs b/src/PowderCoating.Application/DTOs/Job/JobDtos.cs
index b8b8196..83ff0ed 100644
--- a/src/PowderCoating.Application/DTOs/Job/JobDtos.cs
+++ b/src/PowderCoating.Application/DTOs/Job/JobDtos.cs
@@ -404,9 +404,8 @@ public class JobTimeEntryDto
{
public int Id { get; set; }
public int JobId { get; set; }
- public int ShopWorkerId { get; set; }
+ public string? UserId { get; set; }
public string WorkerName { get; set; } = string.Empty;
- public string WorkerRole { get; set; } = string.Empty;
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; }
@@ -417,7 +416,7 @@ public class JobTimeEntryDto
public class CreateJobTimeEntryDto
{
public int JobId { get; set; }
- public int ShopWorkerId { get; set; }
+ public string UserId { get; set; } = string.Empty;
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; }
@@ -427,7 +426,7 @@ public class CreateJobTimeEntryDto
public class UpdateJobTimeEntryDto
{
public int Id { get; set; }
- public int ShopWorkerId { get; set; }
+ public string UserId { get; set; } = string.Empty;
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; }
diff --git a/src/PowderCoating.Application/Interfaces/IPlatformSettingsService.cs b/src/PowderCoating.Application/Interfaces/IPlatformSettingsService.cs
index 06729d1..eb6d938 100644
--- a/src/PowderCoating.Application/Interfaces/IPlatformSettingsService.cs
+++ b/src/PowderCoating.Application/Interfaces/IPlatformSettingsService.cs
@@ -6,6 +6,7 @@ public interface IPlatformSettingsService
{
Task GetAsync(string key);
Task GetBoolAsync(string key, bool defaultValue = false);
+ Task GetIntAsync(string key, int defaultValue);
Task SetAsync(string key, string? value, string? updatedBy = null);
Task> GetAllAsync();
}
diff --git a/src/PowderCoating.Application/Mappings/JobProfile.cs b/src/PowderCoating.Application/Mappings/JobProfile.cs
index 2e206bb..fb4ab4a 100644
--- a/src/PowderCoating.Application/Mappings/JobProfile.cs
+++ b/src/PowderCoating.Application/Mappings/JobProfile.cs
@@ -67,8 +67,8 @@ public class JobProfile : Profile
// JobTimeEntry → JobTimeEntryDto
CreateMap()
- .ForMember(dest => dest.WorkerName, opt => opt.MapFrom(src => src.Worker != null ? src.Worker.Name : string.Empty))
- .ForMember(dest => dest.WorkerRole, opt => opt.MapFrom(src => src.Worker != null ? FormatEnumName(src.Worker.Role.ToString()) : string.Empty));
+ .ForMember(dest => dest.WorkerName, opt => opt.MapFrom(src =>
+ src.UserDisplayName ?? (src.Worker != null ? src.Worker.Name : string.Empty)));
// CreateJobDto to Job
CreateMap()
diff --git a/src/PowderCoating.Core/Entities/JobTimeEntry.cs b/src/PowderCoating.Core/Entities/JobTimeEntry.cs
index 739cfc4..be96cc6 100644
--- a/src/PowderCoating.Core/Entities/JobTimeEntry.cs
+++ b/src/PowderCoating.Core/Entities/JobTimeEntry.cs
@@ -3,7 +3,9 @@ namespace PowderCoating.Core.Entities;
public class JobTimeEntry : BaseEntity
{
public int JobId { get; set; }
- public int ShopWorkerId { get; set; }
+ public int? ShopWorkerId { get; set; } // legacy — kept for entries created before user migration
+ public string? UserId { get; set; } // FK to AspNetUsers
+ public string? UserDisplayName { get; set; } // snapshot of worker name at entry creation time
public DateTime WorkDate { get; set; }
public decimal HoursWorked { get; set; }
public string? Stage { get; set; } // e.g. "Sandblasting", "Coating", "Masking" — free text
@@ -11,5 +13,5 @@ public class JobTimeEntry : BaseEntity
// Navigation
public virtual Job Job { get; set; } = null!;
- public virtual ShopWorker Worker { get; set; } = null!;
+ public virtual ShopWorker? Worker { get; set; } // nullable — only populated for legacy entries
}
diff --git a/src/PowderCoating.Core/Entities/PlatformSettingKeys.cs b/src/PowderCoating.Core/Entities/PlatformSettingKeys.cs
index db1f0f6..852ff62 100644
--- a/src/PowderCoating.Core/Entities/PlatformSettingKeys.cs
+++ b/src/PowderCoating.Core/Entities/PlatformSettingKeys.cs
@@ -15,4 +15,6 @@ public static class PlatformSettingKeys
public const string MaxTenants = "MaxTenants";
public const string SmsEnabled = "SmsEnabled";
public const string AiCatalogPriceCheckEnabled = "AiCatalogPriceCheckEnabled";
+ public const string GracePeriodDays = "GracePeriodDays";
+ public const string GracePeriodAppliesToTrials = "GracePeriodAppliesToTrials";
}
diff --git a/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
index 123fead..5a86b20 100644
--- a/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/src/PowderCoating.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -5000,7 +5000,7 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property("Notes")
.HasColumnType("nvarchar(max)");
- b.Property("ShopWorkerId")
+ b.Property("ShopWorkerId")
.HasColumnType("int");
b.Property("Stage")
@@ -5012,6 +5012,12 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property("UpdatedBy")
.HasColumnType("nvarchar(max)");
+ b.Property("UserDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .HasColumnType("nvarchar(max)");
+
b.Property("WorkDate")
.HasColumnType("datetime2");
@@ -6028,7 +6034,7 @@ namespace PowderCoating.Infrastructure.Migrations
{
Id = 1,
CompanyId = 0,
- CreatedAt = new DateTime(2026, 5, 3, 20, 30, 44, 955, DateTimeKind.Utc).AddTicks(5184),
+ CreatedAt = new DateTime(2026, 5, 5, 23, 10, 14, 763, DateTimeKind.Utc).AddTicks(8603),
Description = "Standard pricing for regular customers",
DiscountPercent = 0m,
IsActive = true,
@@ -6039,7 +6045,7 @@ namespace PowderCoating.Infrastructure.Migrations
{
Id = 2,
CompanyId = 0,
- CreatedAt = new DateTime(2026, 5, 3, 20, 30, 44, 955, DateTimeKind.Utc).AddTicks(5189),
+ CreatedAt = new DateTime(2026, 5, 5, 23, 10, 14, 763, DateTimeKind.Utc).AddTicks(8610),
Description = "5% discount for preferred customers",
DiscountPercent = 5m,
IsActive = true,
@@ -6050,7 +6056,7 @@ namespace PowderCoating.Infrastructure.Migrations
{
Id = 3,
CompanyId = 0,
- CreatedAt = new DateTime(2026, 5, 3, 20, 30, 44, 955, DateTimeKind.Utc).AddTicks(5191),
+ CreatedAt = new DateTime(2026, 5, 5, 23, 10, 14, 763, DateTimeKind.Utc).AddTicks(8612),
Description = "10% discount for premium customers",
DiscountPercent = 10m,
IsActive = true,
@@ -8694,9 +8700,7 @@ namespace PowderCoating.Infrastructure.Migrations
b.HasOne("PowderCoating.Core.Entities.ShopWorker", "Worker")
.WithMany("TimeEntries")
- .HasForeignKey("ShopWorkerId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired();
+ .HasForeignKey("ShopWorkerId");
b.Navigation("Job");
diff --git a/src/PowderCoating.Infrastructure/Repositories/InventoryTransactionRepository.cs b/src/PowderCoating.Infrastructure/Repositories/InventoryTransactionRepository.cs
index 84d965a..e344696 100644
--- a/src/PowderCoating.Infrastructure/Repositories/InventoryTransactionRepository.cs
+++ b/src/PowderCoating.Infrastructure/Repositories/InventoryTransactionRepository.cs
@@ -26,6 +26,7 @@ public class InventoryTransactionRepository : Repository,
.Include(t => t.InventoryItem)
.Include(t => t.PurchaseOrder)
.Include(t => t.Job)
+ .ThenInclude(j => j!.Customer)
.Where(t => !t.IsDeleted);
if (itemId.HasValue)
diff --git a/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs b/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs
index b760b96..497ca52 100644
--- a/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs
+++ b/src/PowderCoating.Infrastructure/Services/InventoryAiLookupService.cs
@@ -834,6 +834,10 @@ Rules:
// machine-readable price, SKU, and product info that would otherwise be lost.
var structuredData = ExtractJsonLdData(html);
+ // Extract visible price text BEFORE stripping HTML as a fallback when
+ // JSON-LD is absent or incomplete (e.g. JS-rendered stores).
+ var htmlPriceSnippet = ExtractHtmlPriceSnippet(html);
+
// Remove script/style blocks
html = System.Text.RegularExpressions.Regex.Replace(
html, @"<(script|style)[^>]*>[\s\S]*?(script|style)>", "",
@@ -851,10 +855,11 @@ Rules:
if (text.Length > maxChars)
text = text[..maxChars] + "…";
- // Prepend structured data + document links — Claude treats these as high-confidence
+ // Prepend structured data + document links + price fallback — Claude treats these as high-confidence
var header = new StringBuilder();
- if (!string.IsNullOrWhiteSpace(structuredData)) header.Append(structuredData);
- if (!string.IsNullOrWhiteSpace(docLinks)) header.Append(docLinks);
+ if (!string.IsNullOrWhiteSpace(structuredData)) header.Append(structuredData);
+ if (!string.IsNullOrWhiteSpace(htmlPriceSnippet)) header.Append(htmlPriceSnippet);
+ if (!string.IsNullOrWhiteSpace(docLinks)) header.Append(docLinks);
if (header.Length > 0) text = header + "\n" + text;
_logger.LogInformation("Fetched {Chars} chars from {Url} (structured data: {HasData}, image: {HasImage})",
@@ -897,6 +902,46 @@ Rules:
return null;
}
+ ///
+ /// Scans raw HTML for visible price elements — common WooCommerce/Shopify price class
+ /// names and itemprop="price" microdata — and returns them as a "[Page Price]" header
+ /// line. This runs on the full, untruncated HTML before tag stripping so prices that
+ /// would fall past the 3,500-char page-text cutoff are still surfaced to Claude.
+ /// Only used when JSON-LD structured data didn't already yield a price.
+ ///
+ private static string? ExtractHtmlPriceSnippet(string html)
+ {
+ // itemprop="price" microdata (e.g. 12.99)
+ var microprice = System.Text.RegularExpressions.Regex.Match(
+ html,
+ @"itemprop=[""']price[""'][^>]*content=[""']([0-9]+\.?[0-9]*)[""']|" +
+ @"itemprop=[""']price[""'][^>]*>[\s]*\$?([0-9]+\.?[0-9]*)",
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+ if (microprice.Success)
+ {
+ var val = (microprice.Groups[1].Value.Trim().Length > 0
+ ? microprice.Groups[1].Value
+ : microprice.Groups[2].Value).Trim();
+ if (!string.IsNullOrEmpty(val))
+ return $"[Page Price] Price found in page microdata: ${val}\n";
+ }
+
+ // WooCommerce / common e-commerce price class names
+ var classPricePattern = System.Text.RegularExpressions.Regex.Match(
+ html,
+ @"class=[""'][^""']*(?:price|woocommerce-Price-amount|product-price|bdi)[^""']*[""'][^>]*>" +
+ @"[\s\S]{0,60}?\$\s*([0-9]+\.[0-9]{2})",
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+ if (classPricePattern.Success)
+ {
+ var val = classPricePattern.Groups[1].Value.Trim();
+ if (!string.IsNullOrEmpty(val))
+ return $"[Page Price] Price found in page HTML: ${val}\n";
+ }
+
+ return null;
+ }
+
///
/// Scans raw HTML for anchor tags linking to SDS or TDS documents and returns them as
/// "[Structured Data]" lines that Claude can read and echo back in its JSON response.
@@ -959,6 +1004,12 @@ Rules:
/// Extracts product name, SKU, and price from JSON-LD structured data blocks.
/// Many e-commerce sites (Shopify, WooCommerce, etc.) embed this in the page HTML
/// even when the visible price is rendered by JavaScript.
+ ///
+ /// Handles three common JSON-LD shapes:
+ /// - Single Product object: {"@type":"Product",...}
+ /// - Top-level array: [{"@type":"Product",...},...]
+ /// - WooCommerce/Yoast @graph wrapper: {"@graph":[{"@type":"Product",...},...]}
+ ///
///
private static string? ExtractJsonLdData(string html)
{
@@ -976,12 +1027,7 @@ Rules:
using var doc = JsonDocument.Parse(jsonText);
var root = doc.RootElement;
- // Handle both single-object and @graph array
- IEnumerable nodes = root.ValueKind == JsonValueKind.Array
- ? root.EnumerateArray()
- : new[] { root };
-
- foreach (var node in nodes)
+ foreach (var node in FlattenJsonLdNodes(root))
{
if (!node.TryGetProperty("@type", out var typeEl)) continue;
@@ -1013,6 +1059,34 @@ Rules:
return sb.Length > 0 ? sb.ToString() : null;
}
+ ///
+ /// Recursively flattens a JSON-LD element into individual nodes for processing.
+ /// Handles a top-level array, a single object, and the WooCommerce/Yoast @graph
+ /// wrapper pattern where the root object has no @type but contains an "@graph" array.
+ ///
+ private static IEnumerable FlattenJsonLdNodes(JsonElement el)
+ {
+ if (el.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var item in el.EnumerateArray())
+ foreach (var n in FlattenJsonLdNodes(item))
+ yield return n;
+ }
+ else if (el.ValueKind == JsonValueKind.Object)
+ {
+ // @graph wrapper: {"@graph": [...]} — recurse into the array
+ if (el.TryGetProperty("@graph", out var graph) && graph.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var n in FlattenJsonLdNodes(graph))
+ yield return n;
+ }
+ else
+ {
+ yield return el;
+ }
+ }
+ }
+
///
/// Dispatches JSON-LD offer extraction for both the single-object form
/// ("offers": { ... }) and the array form ("offers": [ ... ])
diff --git a/src/PowderCoating.Infrastructure/Services/PlatformSettingsService.cs b/src/PowderCoating.Infrastructure/Services/PlatformSettingsService.cs
index 7455549..9115ef0 100644
--- a/src/PowderCoating.Infrastructure/Services/PlatformSettingsService.cs
+++ b/src/PowderCoating.Infrastructure/Services/PlatformSettingsService.cs
@@ -52,6 +52,17 @@ public class PlatformSettingsService : IPlatformSettingsService
return bool.TryParse(value, out var result) ? result : defaultValue;
}
+ ///
+ /// Reads a platform setting as an integer. Returns when
+ /// the key is missing or the stored value cannot be parsed as an integer.
+ ///
+ public async Task GetIntAsync(string key, int defaultValue)
+ {
+ var value = await GetAsync(key);
+ if (value == null) return defaultValue;
+ return int.TryParse(value, out var result) ? result : defaultValue;
+ }
+
///
/// Creates or updates the platform setting identified by .
/// Records (typically the SuperAdmin's username) and
diff --git a/src/PowderCoating.Infrastructure/Services/SubscriptionService.cs b/src/PowderCoating.Infrastructure/Services/SubscriptionService.cs
index 612986d..f96d680 100644
--- a/src/PowderCoating.Infrastructure/Services/SubscriptionService.cs
+++ b/src/PowderCoating.Infrastructure/Services/SubscriptionService.cs
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
+using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
@@ -27,11 +28,13 @@ public class SubscriptionService : ISubscriptionService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ApplicationDbContext _context;
+ private readonly IPlatformSettingsService _platformSettings;
- public SubscriptionService(IUnitOfWork unitOfWork, ApplicationDbContext context)
+ public SubscriptionService(IUnitOfWork unitOfWork, ApplicationDbContext context, IPlatformSettingsService platformSettings)
{
_unitOfWork = unitOfWork;
_context = context;
+ _platformSettings = platformSettings;
}
///
@@ -203,7 +206,8 @@ public class SubscriptionService : ISubscriptionService
if (daysUntil == null || daysUntil > 0)
return SubscriptionStatus.Active;
- if (daysUntil >= -AppConstants.SubscriptionConstants.GracePeriodDays)
+ var graceDays = await GetEffectiveGracePeriodDaysAsync(company);
+ if (daysUntil >= -graceDays)
return SubscriptionStatus.GracePeriod;
return SubscriptionStatus.Expired;
@@ -226,6 +230,25 @@ public class SubscriptionService : ISubscriptionService
return (int)(expiry - today).TotalDays;
}
+ ///
+ /// Returns the effective grace period in days for a company. Trial companies (no Stripe subscription)
+ /// get 0 days unless the GracePeriodAppliesToTrials platform setting is explicitly enabled.
+ /// Paid companies always use the configured GracePeriodDays value.
+ ///
+ private async Task GetEffectiveGracePeriodDaysAsync(Company company)
+ {
+ var isTrial = string.IsNullOrEmpty(company.StripeSubscriptionId);
+ if (isTrial)
+ {
+ var appliesToTrials = await _platformSettings.GetBoolAsync(
+ PlatformSettingKeys.GracePeriodAppliesToTrials, defaultValue: false);
+ if (!appliesToTrials) return 0;
+ }
+ return await _platformSettings.GetIntAsync(
+ PlatformSettingKeys.GracePeriodDays,
+ AppConstants.SubscriptionConstants.GracePeriodDays);
+ }
+
///
/// Returns the total catalog item count (including soft-deleted) and the plan maximum.
/// Returns a max of -1 when the plan config is absent (unlimited by default).
diff --git a/src/PowderCoating.Shared/Constants/AppConstants.cs b/src/PowderCoating.Shared/Constants/AppConstants.cs
index a549144..ab94f7b 100644
--- a/src/PowderCoating.Shared/Constants/AppConstants.cs
+++ b/src/PowderCoating.Shared/Constants/AppConstants.cs
@@ -2,7 +2,7 @@ namespace PowderCoating.Shared.Constants;
public static class AppConstants
{
- public const string ApplicationName = "Powder Coating Management System";
+ public const string ApplicationName = "Powder Coating Logix";
public const string Version = "1.0.0";
/// Set to true to enable SMS features throughout the UI.
diff --git a/src/PowderCoating.Web/BackgroundServices/SubscriptionExpiryBackgroundService.cs b/src/PowderCoating.Web/BackgroundServices/SubscriptionExpiryBackgroundService.cs
index e44c0e8..6b0e0ea 100644
--- a/src/PowderCoating.Web/BackgroundServices/SubscriptionExpiryBackgroundService.cs
+++ b/src/PowderCoating.Web/BackgroundServices/SubscriptionExpiryBackgroundService.cs
@@ -80,6 +80,12 @@ public class SubscriptionExpiryBackgroundService : BackgroundService
var db = scope.ServiceProvider.GetRequiredService();
var emailService = scope.ServiceProvider.GetRequiredService();
var adminNotification = scope.ServiceProvider.GetRequiredService();
+ var platformSettings = scope.ServiceProvider.GetRequiredService();
+ var gracePeriodDays = await platformSettings.GetIntAsync(
+ PlatformSettingKeys.GracePeriodDays,
+ AppConstants.SubscriptionConstants.GracePeriodDays);
+ var gracePeriodAppliesToTrials = await platformSettings.GetBoolAsync(
+ PlatformSettingKeys.GracePeriodAppliesToTrials, defaultValue: false);
var today = DateTime.UtcNow.Date;
@@ -100,7 +106,9 @@ public class SubscriptionExpiryBackgroundService : BackgroundService
foreach (var company in companies)
{
if (ct.IsCancellationRequested) break;
- await ProcessCompanyAsync(db, emailService, adminNotification, company, today, ct);
+ var isTrial = string.IsNullOrEmpty(company.StripeSubscriptionId);
+ var effectiveGraceDays = isTrial && !gracePeriodAppliesToTrials ? 0 : gracePeriodDays;
+ await ProcessCompanyAsync(db, emailService, adminNotification, company, today, effectiveGraceDays, ct);
}
await db.SaveChangesAsync(ct);
@@ -124,10 +132,10 @@ public class SubscriptionExpiryBackgroundService : BackgroundService
IAdminNotificationService adminNotification,
PowderCoating.Core.Entities.Company company,
DateTime today,
+ int gracePeriodDays,
CancellationToken ct)
{
var endDate = company.SubscriptionEndDate!.Value.Date;
- var gracePeriodDays = AppConstants.SubscriptionConstants.GracePeriodDays;
var expiredDate = endDate.AddDays(gracePeriodDays);
// ── Status transitions ────────────────────────────────────────────
@@ -167,6 +175,7 @@ public class SubscriptionExpiryBackgroundService : BackgroundService
await SendEmailIfNotSentAsync(db, emailService, company, today,
NotificationType.SubscriptionExpiryReminder,
daysBeforeExpiry: 0,
+ gracePeriodDays,
ct);
// Notify platform admin
@@ -184,6 +193,7 @@ public class SubscriptionExpiryBackgroundService : BackgroundService
await SendEmailIfNotSentAsync(db, emailService, company, today,
NotificationType.SubscriptionExpiryReminder,
daysBeforeExpiry: daysUntilExpiry,
+ gracePeriodDays,
ct);
}
}
@@ -203,6 +213,7 @@ public class SubscriptionExpiryBackgroundService : BackgroundService
DateTime today,
NotificationType notificationType,
int daysBeforeExpiry,
+ int gracePeriodDays,
CancellationToken ct)
{
if (string.IsNullOrEmpty(company.PrimaryContactEmail)) return;
@@ -223,7 +234,7 @@ public class SubscriptionExpiryBackgroundService : BackgroundService
if (alreadySent) return;
- var (subject, plain, html) = BuildEmail(company, daysBeforeExpiry);
+ var (subject, plain, html) = BuildEmail(company, daysBeforeExpiry, gracePeriodDays);
var (success, error) = await emailService.SendEmailAsync(
company.PrimaryContactEmail,
@@ -255,14 +266,15 @@ public class SubscriptionExpiryBackgroundService : BackgroundService
///
private static (string subject, string plain, string html) BuildEmail(
PowderCoating.Core.Entities.Company company,
- int daysBeforeExpiry)
+ int daysBeforeExpiry,
+ int gracePeriodDays)
{
var endDate = company.SubscriptionEndDate!.Value.Date;
if (daysBeforeExpiry == 0)
{
// Grace period notice
- var graceDays = AppConstants.SubscriptionConstants.GracePeriodDays;
+ var graceDays = gracePeriodDays;
var expiredOn = endDate.AddDays(graceDays).ToString("MMMM d, yyyy");
var subject = $"[0d] Your Powder Coating Logix subscription has expired";
var plain = $"Hi {company.CompanyName},\n\n" +
diff --git a/src/PowderCoating.Web/Middleware/SubscriptionMiddleware.cs b/src/PowderCoating.Web/Middleware/SubscriptionMiddleware.cs
index f20d326..86009f0 100644
--- a/src/PowderCoating.Web/Middleware/SubscriptionMiddleware.cs
+++ b/src/PowderCoating.Web/Middleware/SubscriptionMiddleware.cs
@@ -1,3 +1,5 @@
+using PowderCoating.Application.Interfaces;
+using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
@@ -152,7 +154,16 @@ public class SubscriptionMiddleware
var daysUntil = subscriptionService.DaysUntilExpiry(company);
if (daysUntil != null)
{
- if (daysUntil < -AppConstants.SubscriptionConstants.GracePeriodDays)
+ var platformSettings = context.RequestServices.GetRequiredService();
+ var isTrial = string.IsNullOrEmpty(company.StripeSubscriptionId);
+ var graceDays = 0;
+ if (!isTrial || await platformSettings.GetBoolAsync(PlatformSettingKeys.GracePeriodAppliesToTrials))
+ {
+ graceDays = await platformSettings.GetIntAsync(
+ PlatformSettingKeys.GracePeriodDays,
+ AppConstants.SubscriptionConstants.GracePeriodDays);
+ }
+ if (daysUntil < -graceDays)
{
// Past grace period — hard lockout
context.Response.Redirect("/Billing/Expired");
diff --git a/src/PowderCoating.Web/Properties/PublishProfiles/linuxpcl - Zip Deploy.pubxml b/src/PowderCoating.Web/Properties/PublishProfiles/linuxpcl - Zip Deploy.pubxml
index 0f636da..2426c9c 100644
--- a/src/PowderCoating.Web/Properties/PublishProfiles/linuxpcl - Zip Deploy.pubxml
+++ b/src/PowderCoating.Web/Properties/PublishProfiles/linuxpcl - Zip Deploy.pubxml
@@ -17,7 +17,7 @@ by editing this MSBuild file. In order to learn more about this please visit htt
Any CPU
f6a7b8c9-d0e1-4f5a-3b4c-5d6e7f8a9b0c
https://linuxpcl-cvatbmbch9cchmbe.scm.centralus-01.azurewebsites.net/
-
+ $linuxpcl
<_SavePWD>false
\ No newline at end of file
diff --git a/src/PowderCoating.Web/Properties/launchSettings.json b/src/PowderCoating.Web/Properties/launchSettings.json
index 8a9d233..fb1fd88 100644
--- a/src/PowderCoating.Web/Properties/launchSettings.json
+++ b/src/PowderCoating.Web/Properties/launchSettings.json
@@ -6,7 +6,7 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
- "applicationUrl": "https://localhost:58461;http://localhost:58462"
+ "applicationUrl": "https://localhost:58461;http://localhost:58462;http://0.0.0.0:58462"
}
}
}
\ No newline at end of file