91 lines
3.2 KiB
C#
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);
|
|
}
|
|
}
|