using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tiku.Application.Catalog; using Tiku.Application.QuestionBanks; using Tiku.Application.Security; using Tiku.Domain.Catalog; using Tiku.Domain.Content; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; namespace Tiku.Infrastructure.Catalog; public sealed class TaxonomyService( TikuDbContext dbContext, IPublicQuestionAccessPolicy accessPolicy, ITenantExecutionScope tenantExecutionScope) : ITaxonomyService { public async Task> ListAsync( Guid tenantId, CancellationToken cancellationToken = default) { var includePlatform = true; try { await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken); } catch (PublicQuestionAccessDeniedException) { includePlatform = false; } return await tenantExecutionScope.ExecuteAsync( new SystemScopeRequest( tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(TaxonomyService), "List platform taxonomy with tenant extensions", Guid.NewGuid().ToString("N")), async (provider, token) => { var systemDbContext = provider.GetRequiredService(); var platformTenantId = includePlatform ? await systemDbContext.Tenants.AsNoTracking() .Where(tenant => tenant.Mode == TenantMode.PlatformOwned) .Select(tenant => (Guid?)tenant.Id) .SingleOrDefaultAsync(token) : null; return await systemDbContext.TaxonomyNodes.AsNoTracking() .Where(node => node.IsActive && (node.TenantId == tenantId || (platformTenantId.HasValue && node.TenantId == platformTenantId.Value))) .OrderBy(node => node.Depth) .ThenBy(node => node.SortOrder) .Select(node => new TaxonomyNodeItem( node.Id, node.TenantId == tenantId ? QuestionSource.Tenant : QuestionSource.Platform, node.ParentId, !node.ParentOwnerTenantId.HasValue ? null : node.ParentOwnerTenantId == tenantId ? QuestionSource.Tenant : QuestionSource.Platform, node.NodeType, node.Code, node.Name, node.Path, node.Depth, node.SortOrder, node.Metadata)) .ToArrayAsync(token); }, cancellationToken); } public async Task CreateAsync( Guid tenantId, CreateTaxonomyNodeCommand command, CancellationToken cancellationToken = default) { Guid? parentOwnerTenantId = null; TaxonomyParent? parent = null; if (command.ParentId.HasValue) { parentOwnerTenantId = command.ParentSource switch { QuestionSource.Tenant => tenantId, QuestionSource.Platform => await ResolvePlatformTenantIdAsync(tenantId, cancellationToken), _ => throw new InvalidOperationException("A parent source is required when parentId is provided.") }; parent = await tenantExecutionScope.ExecuteAsync( new SystemScopeRequest( tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(TaxonomyService), "Validate taxonomy extension parent ownership", Guid.NewGuid().ToString("N")), async (provider, token) => { var systemDbContext = provider.GetRequiredService(); return await systemDbContext.TaxonomyNodes.AsNoTracking() .Where(node => node.TenantId == parentOwnerTenantId && node.Id == command.ParentId.Value && node.IsActive) .Select(node => new TaxonomyParent(node.Path, node.Depth)) .SingleOrDefaultAsync(token); }, cancellationToken) ?? throw new InvalidOperationException("Taxonomy parent was not found."); } var node = new TaxonomyNode { TenantId = tenantId, ParentOwnerTenantId = parentOwnerTenantId, ParentId = command.ParentId, NodeType = command.NodeType, Code = command.Code.Trim(), Name = command.Name.Trim(), Depth = parent is null ? 0 : parent.Depth + 1, SortOrder = command.SortOrder, Metadata = command.Metadata.Clone() }; node.Path = parent is null ? $"n{node.Id:N}" : $"{parent.Path}.n{node.Id:N}"; dbContext.TaxonomyNodes.Add(node); await dbContext.SaveChangesAsync(cancellationToken); return new TaxonomyNodeItem( node.Id, QuestionSource.Tenant, node.ParentId, node.ParentId.HasValue ? command.ParentSource : null, node.NodeType, node.Code, node.Name, node.Path, node.Depth, node.SortOrder, node.Metadata); } private async Task ResolvePlatformTenantIdAsync(Guid tenantId, CancellationToken cancellationToken) { await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken); return await tenantExecutionScope.ExecuteAsync( new SystemScopeRequest( tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(TaxonomyService), "Resolve platform taxonomy owner", Guid.NewGuid().ToString("N")), async (provider, token) => await provider.GetRequiredService() .Tenants.AsNoTracking() .Where(tenant => tenant.Mode == TenantMode.PlatformOwned) .Select(tenant => tenant.Id) .SingleAsync(token), cancellationToken); } private sealed record TaxonomyParent(string? Path, int Depth); }