Commit remaining unstaged changes from this session

- Platform settings service: IPlatformSettingsService, PlatformSettingKeys,
  PlatformSettingsService, SubscriptionService, AppConstants,
  SubscriptionExpiryBackgroundService, SubscriptionMiddleware
- JobTimeEntry entity, DTOs, AutoMapper profile (ShopWorker → UserId migration)
- InventoryDtos: SourceTransactionId on PowderUsageLogDto
- InventoryTransactionRepository: include Job.Customer in ledger query
- InventoryAiLookupService: @graph unwrap + HTML price fallback
- ApplicationDbContextModelSnapshot: reflect migration changes
- launchSettings.json, publish profile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 21:20:30 -04:00
parent 4c070e7487
commit 20ae11be03
16 changed files with 177 additions and 35 deletions
@@ -80,6 +80,12 @@ public class SubscriptionExpiryBackgroundService : BackgroundService
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
var adminNotification = scope.ServiceProvider.GetRequiredService<IAdminNotificationService>();
var platformSettings = scope.ServiceProvider.GetRequiredService<IPlatformSettingsService>();
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
/// </summary>
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" +
@@ -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<IPlatformSettingsService>();
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");
@@ -17,7 +17,7 @@ by editing this MSBuild file. In order to learn more about this please visit htt
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<ProjectGuid>f6a7b8c9-d0e1-4f5a-3b4c-5d6e7f8a9b0c</ProjectGuid>
<PublishUrl>https://linuxpcl-cvatbmbch9cchmbe.scm.centralus-01.azurewebsites.net/</PublishUrl>
<UserName />
<UserName>$linuxpcl</UserName>
<_SavePWD>false</_SavePWD>
</PropertyGroup>
</Project>
@@ -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"
}
}
}