PlotDirector/PlotLine/Services/StoryIntelligenceOperationalServices.cs

91 lines
3.2 KiB
C#

using PlotLine.Data;
using PlotLine.Models;
namespace PlotLine.Services;
public interface IStoryIntelligenceEntitlementService
{
Task EnsureCanQueueAsync(int userId);
Task EnsureCanExecuteAsync(int userId);
}
public sealed class StoryIntelligenceEntitlementService(ISubscriptionService subscriptions) : IStoryIntelligenceEntitlementService
{
public Task EnsureCanQueueAsync(int userId) => EnsureAllowedAsync(userId);
public Task EnsureCanExecuteAsync(int userId) => EnsureAllowedAsync(userId);
private async Task EnsureAllowedAsync(int userId)
{
var subscription = await subscriptions.GetCurrentSubscriptionAsync(userId);
if (!IsActive(subscription, DateTime.UtcNow))
{
throw new SubscriptionLimitException("Story Intelligence requires an active subscription before paid AI work can be queued or processed.");
}
}
private static bool IsActive(UserSubscriptionDetail? subscription, DateTime utcNow)
{
if (subscription is null || !subscription.IsActive || !subscription.SubscriptionLevelIsActive)
{
return false;
}
if (subscription.EndDateUTC.HasValue && subscription.EndDateUTC.Value <= utcNow)
{
return false;
}
if (subscription.CurrentPeriodEndUTC.HasValue
&& subscription.CurrentPeriodEndUTC.Value <= utcNow
&& !string.IsNullOrWhiteSpace(subscription.StripeSubscriptionId))
{
return false;
}
var status = (subscription.StripeStatus ?? string.Empty).Trim().ToLowerInvariant();
return status is not ("canceled" or "cancelled" or "incomplete_expired" or "ended" or "expired" or "inactive");
}
}
public interface IStoryIntelligenceCompletionNotificationService
{
Task NotifyIfImportTerminalAsync(StoryIntelligenceQueuedRun run, CancellationToken cancellationToken = default);
}
public sealed class StoryIntelligenceCompletionNotificationService(
IStoryIntelligencePipelineRepository pipelines,
IUserRepository users,
IEmailService emails,
ILogger<StoryIntelligenceCompletionNotificationService> logger) : IStoryIntelligenceCompletionNotificationService
{
public async Task NotifyIfImportTerminalAsync(StoryIntelligenceQueuedRun run, CancellationToken cancellationToken = default)
{
if (run.BookID is not int bookId)
{
return;
}
var target = await pipelines.TryBeginCompletionNotificationAsync(bookId, run.UserID);
if (target is null)
{
return;
}
var user = await users.GetByIdAsync(target.UserID);
if (user is null || string.IsNullOrWhiteSpace(user.Email))
{
logger.LogWarning("Story Intelligence import notification skipped because user {UserID} has no email target.", target.UserID);
return;
}
await emails.SendStoryIntelligenceImportNotificationAsync(user.Email, user.DisplayName, target);
logger.LogInformation(
"Queued Story Intelligence import {NotificationKind} notification for import session {ImportSessionID}, book {BookID}, user {UserID}.",
target.NotificationKind,
target.ImportSessionID,
target.BookID,
target.UserID);
}
}