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 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 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 { 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("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 providers) => new(code, required, providers.Contains(capability), providers.Contains(capability) ? "active" : "missing"); }