86 lines
5.3 KiB
C#
86 lines
5.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Application.Tenancy;
|
|
using Tiku.Domain.Platform;
|
|
using Tiku.Domain.Tenancy;
|
|
using Tiku.Infrastructure.Persistence;
|
|
|
|
namespace Tiku.Infrastructure.Tenancy;
|
|
|
|
internal sealed class TenantOnboardingService(
|
|
TikuDbContext dbContext,
|
|
IFeatureAccessService featureAccessService) : ITenantOnboardingService
|
|
{
|
|
public async Task<TenantOnboardingStatus> GetStatusAsync(
|
|
Guid tenantId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var tenant = await dbContext.Tenants.AsNoTracking().SingleOrDefaultAsync(value => value.Id == tenantId, cancellationToken)
|
|
?? throw new TenantExternalProviderException("Tenant was not found.", "tenant_not_found");
|
|
var ownerActivated = tenant.OwnerUserId.HasValue && await (
|
|
from membership in dbContext.TenantMemberships.AsNoTracking()
|
|
join user in dbContext.Users.AsNoTracking() on membership.UserId equals user.Id
|
|
where membership.TenantId == tenantId && membership.UserId == tenant.OwnerUserId &&
|
|
membership.Role == TenantRole.TenantOwner && membership.Status == MembershipStatus.Active &&
|
|
user.Status == Tiku.Domain.Identity.UserStatus.Active && !user.ForcePasswordChange
|
|
select membership.Id).AnyAsync(cancellationToken);
|
|
var now = DateTimeOffset.UtcNow;
|
|
var subscriptionActive = await dbContext.TenantSaasSubscriptions.AsNoTracking().AnyAsync(value =>
|
|
value.TenantId == tenantId &&
|
|
(value.Status == TenantSaasSubscriptionStatus.Trial || value.Status == TenantSaasSubscriptionStatus.Active) &&
|
|
value.StartsAt <= now && value.CurrentPeriodEnd > now, cancellationToken);
|
|
var billingPolicyConfigured = await dbContext.TenantBillingPolicies.AsNoTracking().AnyAsync(value =>
|
|
value.TenantId == tenantId && value.RenewalLeadDays >= 1 && value.RenewalLeadDays <= 90 &&
|
|
value.DefaultPaymentProvider != string.Empty, cancellationToken);
|
|
var primaryDomainActive = await dbContext.TenantDomains.AsNoTracking().AnyAsync(value =>
|
|
value.TenantId == tenantId && value.IsPrimary && value.Status == TenantDomainStatus.Active, cancellationToken);
|
|
var frontendPublished = await dbContext.TenantFrontendConfigs.AsNoTracking().AnyAsync(value =>
|
|
value.TenantId == tenantId && value.PublishedAt != null, cancellationToken);
|
|
var loginMethods = await dbContext.TenantAuthPolicies.AsNoTracking()
|
|
.Where(value => value.TenantId == tenantId)
|
|
.Select(value => value.AllowedStudentLoginMethods)
|
|
.SingleOrDefaultAsync(cancellationToken) ?? ["password"];
|
|
var hasLoginMethod = loginMethods.Any(value => !string.IsNullOrWhiteSpace(value));
|
|
|
|
var enabledFeatures = await featureAccessService.GetEnabledFeaturesAsync(tenantId, FeatureAccessOperation.Read, cancellationToken);
|
|
var paymentRequired = enabledFeatures.Contains(SaasFeatureCatalog.StudentStore);
|
|
var storageRequired = enabledFeatures.Overlaps(new[]
|
|
{
|
|
SaasFeatureCatalog.PrivateQuestionBank,
|
|
SaasFeatureCatalog.Video,
|
|
SaasFeatureCatalog.Handbook
|
|
});
|
|
var smsRequired = loginMethods.Any(value => string.Equals(value, "sms", StringComparison.OrdinalIgnoreCase));
|
|
var providers = await dbContext.TenantExternalProviders.AsNoTracking()
|
|
.Where(value => value.TenantId == tenantId && value.Status == TenantExternalProviderStatus.Active)
|
|
.Select(value => value.Capability)
|
|
.Distinct()
|
|
.ToArrayAsync(cancellationToken);
|
|
|
|
var steps = new List<TenantOnboardingStep>
|
|
{
|
|
new("tenant_active", true, tenant.Status == TenantStatus.Active, tenant.Status.ToString()),
|
|
new("owner_activated", true, ownerActivated, tenant.OwnerUserId?.ToString()),
|
|
new("subscription_active", true, subscriptionActive, null),
|
|
new("billing_policy_configured", true, billingPolicyConfigured, null),
|
|
new("primary_domain_active", true, primaryDomainActive, null),
|
|
new("frontend_config_published", true, frontendPublished, null),
|
|
new("student_login_configured", true, hasLoginMethod, string.Join(',', loginMethods)),
|
|
ProviderStep("object_storage_provider", TenantExternalProviderCapability.ObjectStorage, storageRequired, providers),
|
|
ProviderStep("sms_provider", TenantExternalProviderCapability.Sms, smsRequired, providers),
|
|
ProviderStep("payment_provider", TenantExternalProviderCapability.Payment, paymentRequired, providers),
|
|
ProviderStep("notification_provider", TenantExternalProviderCapability.Notification, false, providers)
|
|
};
|
|
var required = steps.Count(value => value.Required);
|
|
var completed = steps.Count(value => value.Required && value.Completed);
|
|
return new TenantOnboardingStatus(tenantId, completed == required, completed, required, steps);
|
|
}
|
|
|
|
private static TenantOnboardingStep ProviderStep(
|
|
string code,
|
|
TenantExternalProviderCapability capability,
|
|
bool required,
|
|
IReadOnlyCollection<TenantExternalProviderCapability> providers) =>
|
|
new(code, required, providers.Contains(capability), providers.Contains(capability) ? "active" : "missing");
|
|
}
|