Spaces:
No application file
No application file
| // ============================================================ | |
| // REPOSITORY STUB IMPLEMENTATIONS | |
| // These are functional local-dev implementations. | |
| // Replace with full implementations per module. | |
| // ============================================================ | |
| using Microsoft.EntityFrameworkCore; | |
| using Microsoft.Extensions.Caching.Distributed; | |
| using OptimAI.BRE.AIEngine.Api; | |
| using OptimAI.BRE.RuleDesigner.Api; | |
| using OptimAI.BRE.RuleEngine.Api; | |
| using OptimAI.BRE.RuleEngine.Application; | |
| using OptimAI.BRE.RuleEngine.Domain; | |
| using OptimAI.BRE.Shared.Domain; | |
| using System.Text.Json; | |
| namespace OptimAI.BRE.RuleEngine.Infrastructure; | |
| // ---- DynamicFieldResolver ---- | |
| public class DynamicFieldResolver : IDynamicFieldResolver | |
| { | |
| public object? Resolve(string fieldPath, DynamicDataContext context) => context.GetValue(fieldPath); | |
| public bool TryResolve(string fieldPath, DynamicDataContext context, out object? value) | |
| { | |
| value = context.GetValue(fieldPath); | |
| return true; | |
| } | |
| } | |
| // ---- DeviationTypeRepository ---- | |
| public class DeviationTypeRepository : IDeviationTypeRepository | |
| { | |
| private readonly BREDbContext _db; | |
| public DeviationTypeRepository(BREDbContext db) => _db = db; | |
| public async Task<DeviationType?> FindByCodeAsync(Guid tenantId, string code, CancellationToken ct = default) | |
| => await _db.DeviationTypes.FirstOrDefaultAsync(d => | |
| (d.TenantId == tenantId || d.TenantId == null) && d.DeviationCode == code, ct); | |
| public async Task<IReadOnlyList<DeviationType>> GetAllActiveAsync(Guid tenantId, CancellationToken ct = default) | |
| => await _db.DeviationTypes | |
| .Where(d => (d.TenantId == tenantId || d.TenantId == null) && d.IsActive) | |
| .ToListAsync(ct); | |
| } | |
| // ---- RiskWeightRepository ---- | |
| public class RiskWeightRepository : IRiskWeightRepository | |
| { | |
| public Task<Dictionary<string, decimal>> GetWeightsAsync(Guid tenantId, string? productCode = null) | |
| => Task.FromResult(new Dictionary<string, decimal> | |
| { | |
| ["bureau"] = 30, ["income"] = 25, ["fi"] = 15, | |
| ["fraud"] = 15, ["vehicle"] = 10, ["employment"] = 10, ["rule_penalty"] = 5 | |
| }); | |
| } | |
| // ---- ExecutionRequestRepository ---- | |
| public class ExecutionRequestRepository : IExecutionRequestRepository | |
| { | |
| private readonly BREDbContext _db; | |
| public ExecutionRequestRepository(BREDbContext db) => _db = db; | |
| public async Task<ExecutionRequest> CreateAsync(ExecutionRequest request, CancellationToken ct = default) | |
| { | |
| _db.ExecutionRequests.Add(request); | |
| await _db.SaveChangesAsync(ct); | |
| return request; | |
| } | |
| public async Task MarkCompletedAsync(Guid id, CancellationToken ct = default) | |
| { | |
| await _db.ExecutionRequests.Where(r => r.Id == id) | |
| .ExecuteUpdateAsync(s => s | |
| .SetProperty(r => r.Status, ExecutionStatus.Completed) | |
| .SetProperty(r => r.CompletedAt, DateTime.UtcNow), ct); | |
| } | |
| } | |
| // ---- ExecutionResultRepository ---- | |
| public class ExecutionResultRepository : IExecutionResultRepository | |
| { | |
| private readonly BREDbContext _db; | |
| public ExecutionResultRepository(BREDbContext db) => _db = db; | |
| public async Task SaveAsync(ExecutionResult result, CancellationToken ct = default) | |
| { | |
| _db.ExecutionResults.Add(result); | |
| await _db.SaveChangesAsync(ct); | |
| } | |
| public async Task<ExecutionResult?> GetByRequestIdAsync(Guid requestId, Guid tenantId, CancellationToken ct = default) | |
| => await _db.ExecutionResults.FirstOrDefaultAsync(r => r.RequestId == requestId && r.TenantId == tenantId, ct); | |
| public async Task<PagedResponse<DecisionSummaryDto>> GetHistoryAsync(DecisionHistoryQuery query, CancellationToken ct = default) | |
| { | |
| var q = _db.ExecutionResults | |
| .Join(_db.ExecutionRequests, r => r.RequestId, req => req.Id, (r, req) => new { r, req }) | |
| .Where(x => x.r.TenantId == query.TenantId); | |
| if (query.Decision != null) q = q.Where(x => x.r.FinalDecision.ToString() == query.Decision); | |
| if (query.FromDate.HasValue) q = q.Where(x => x.req.CreatedAt >= query.FromDate); | |
| if (query.ToDate.HasValue) q = q.Where(x => x.req.CreatedAt <= query.ToDate); | |
| var total = await q.CountAsync(ct); | |
| var items = await q.OrderByDescending(x => x.req.CreatedAt) | |
| .Skip((query.Page - 1) * query.PageSize) | |
| .Take(query.PageSize) | |
| .Select(x => new DecisionSummaryDto | |
| { | |
| RequestId = x.r.RequestId, | |
| ApplicationId = x.req.ApplicationId, | |
| Decision = x.r.FinalDecision.ToString(), | |
| TrafficLight = x.r.TrafficLight.ToString() ?? "AMBER", | |
| RiskScore = x.r.RiskScore ?? 0, | |
| DeviationsCount = x.r.DeviationsCount, | |
| CreatedAt = x.req.CreatedAt | |
| }) | |
| .ToListAsync(ct); | |
| return new PagedResponse<DecisionSummaryDto> | |
| { | |
| Items = items, TotalCount = total, | |
| Page = query.Page, PageSize = query.PageSize | |
| }; | |
| } | |
| } | |
| // ---- DecisionReportService ---- | |
| public class DecisionReportService : IDecisionReportService | |
| { | |
| private readonly BREDbContext _db; | |
| public DecisionReportService(BREDbContext db) => _db = db; | |
| public async Task<DecisionReport?> GenerateAsync(Guid requestId, ExecutionResult result, CancellationToken ct = default) | |
| { | |
| var report = new DecisionReportEntity | |
| { | |
| RequestId = requestId, | |
| TenantId = result.TenantId, | |
| FinalDecision = result.FinalDecision.ToString(), | |
| RiskScore = result.RiskScore, | |
| RiskCategory = result.RiskCategory?.ToString(), | |
| TrafficLight = result.TrafficLight?.ToString(), | |
| ReportJson = JsonSerializer.Serialize(result), | |
| GeneratedAt = DateTime.UtcNow | |
| }; | |
| _db.DecisionReports.Add(report); | |
| await _db.SaveChangesAsync(ct); | |
| return new DecisionReport { Id = report.Id }; | |
| } | |
| } | |
| // ---- SandboxService ---- | |
| public class SandboxService : ISandboxService | |
| { | |
| private readonly IRuleExecutionService _exec; | |
| private readonly IRuleLoader _loader; | |
| public SandboxService(IRuleExecutionService exec, IRuleLoader loader) | |
| { | |
| _exec = exec; | |
| _loader = loader; | |
| } | |
| public async Task<object> SimulateAsync(SandboxRequest request, CancellationToken ct = default) | |
| { | |
| var context = new ExecutionContext | |
| { | |
| TenantId = request.TenantId, | |
| CorrelationId = $"sandbox-{Guid.NewGuid()}", | |
| RuleSetId = request.RuleSetId, | |
| Data = new DynamicDataContext(request.TestData), | |
| Options = new ExecutionOptions { EnableAiAnalysis = false } | |
| }; | |
| return await _exec.ExecuteAsync(context, ct); | |
| } | |
| } | |
| // ---- AiPromptRepository ---- | |
| public class AiPromptRepository : IAiPromptRepository | |
| { | |
| private readonly BREDbContext _db; | |
| public AiPromptRepository(BREDbContext db) => _db = db; | |
| public async Task<string?> GetPromptAsync(string promptCode, CancellationToken ct = default) | |
| { | |
| var p = await _db.Set<AiPromptEntity>() | |
| .FirstOrDefaultAsync(x => x.PromptCode == promptCode && x.IsActive, ct); | |
| return p?.PromptTemplate; | |
| } | |
| } | |
| // ---- AiRuleGeneratorRepository ---- | |
| public class AiRuleGeneratorRepository : IAiRuleGeneratorRepository | |
| { | |
| private readonly BREDbContext _db; | |
| public AiRuleGeneratorRepository(BREDbContext db) => _db = db; | |
| public async Task<Guid> SaveGeneratedRuleAsync(AiGeneratedRuleRecord record, CancellationToken ct = default) | |
| { | |
| var entity = new AiGeneratedRuleEntity | |
| { | |
| TenantId = record.TenantId, | |
| UserPrompt = record.UserPrompt, | |
| GeneratedRule = JsonSerializer.Serialize(record.GeneratedRule), | |
| CreatedBy = record.CreatedBy | |
| }; | |
| _db.AiGeneratedRules.Add(entity); | |
| await _db.SaveChangesAsync(ct); | |
| return entity.Id; | |
| } | |
| public async Task<bool> AcceptRuleAsync(Guid generationId, Guid tenantId, Guid? categoryId, Guid userId, CancellationToken ct = default) | |
| { | |
| var entity = await _db.AiGeneratedRules | |
| .FirstOrDefaultAsync(e => e.Id == generationId && e.TenantId == tenantId, ct); | |
| if (entity == null) return false; | |
| entity.IsAccepted = true; | |
| await _db.SaveChangesAsync(ct); | |
| return true; | |
| } | |
| public async Task<List<AiGeneratedRuleRecord>> GetHistoryAsync(Guid tenantId, int page, int pageSize, CancellationToken ct = default) | |
| { | |
| return await _db.AiGeneratedRules | |
| .Where(e => e.TenantId == tenantId) | |
| .OrderByDescending(e => e.CreatedAt) | |
| .Skip((page - 1) * pageSize).Take(pageSize) | |
| .Select(e => new AiGeneratedRuleRecord | |
| { | |
| Id = e.Id, TenantId = e.TenantId, UserPrompt = e.UserPrompt, | |
| IsAccepted = e.IsAccepted, CreatedBy = e.CreatedBy, CreatedAt = e.CreatedAt | |
| }) | |
| .ToListAsync(ct); | |
| } | |
| } | |
| // ---- FieldCatalogRepository ---- | |
| public class FieldCatalogRepository : IFieldCatalogRepository | |
| { | |
| private readonly BREDbContext _db; | |
| public FieldCatalogRepository(BREDbContext db) => _db = db; | |
| public async Task<List<string>> GetFieldPathsAsync(Guid tenantId, CancellationToken ct = default) | |
| => await _db.FieldCatalog | |
| .Where(f => f.IsActive && (f.TenantId == null || f.TenantId == tenantId)) | |
| .Select(f => f.FieldPath).ToListAsync(ct); | |
| public async Task<List<FieldCatalogDto>> GetCatalogAsync(Guid tenantId, string? category = null, CancellationToken ct = default) | |
| { | |
| var q = _db.FieldCatalog.Where(f => f.IsActive && (f.TenantId == null || f.TenantId == tenantId)); | |
| if (category != null) q = q.Where(f => f.Category == category); | |
| return await q.Select(f => new FieldCatalogDto | |
| { | |
| FieldPath = f.FieldPath, DisplayName = f.DisplayName, | |
| DataType = f.DataType, Category = f.Category, Description = f.Description | |
| }).ToListAsync(ct); | |
| } | |
| } | |
| // ---- RuleRepository ---- | |
| public class RuleRepository : IRuleRepository | |
| { | |
| private readonly BREDbContext _db; | |
| public RuleRepository(BREDbContext db) => _db = db; | |
| public async Task<PagedResponse<RuleSummaryDto>> GetPagedAsync(RuleQuery query, CancellationToken ct = default) | |
| { | |
| var q = _db.Rules.Where(r => r.TenantId == query.TenantId).IgnoreQueryFilters(); | |
| if (query.Status != null && Enum.TryParse<RuleStatus>(query.Status, true, out var s)) | |
| q = q.Where(r => r.Status == s); | |
| if (query.RuleType != null && Enum.TryParse<RuleType>(query.RuleType, true, out var rt)) | |
| q = q.Where(r => r.RuleType == rt); | |
| if (!string.IsNullOrWhiteSpace(query.Search)) | |
| q = q.Where(r => r.RuleName.Contains(query.Search) || r.RuleCode.Contains(query.Search)); | |
| var total = await q.CountAsync(ct); | |
| var items = await q.OrderBy(r => r.Priority).ThenBy(r => r.RuleName) | |
| .Skip((query.Page - 1) * query.PageSize).Take(query.PageSize) | |
| .Select(r => new RuleSummaryDto | |
| { | |
| Id = r.Id, RuleCode = r.RuleCode, RuleName = r.RuleName, | |
| RuleType = r.RuleType.ToString(), Status = r.Status.ToString(), | |
| IsActive = r.IsActive, IsPublished = r.IsPublished, | |
| Priority = r.Priority, Tags = r.Tags, | |
| CreatedAt = r.CreatedAt, UpdatedAt = r.UpdatedAt | |
| }).ToListAsync(ct); | |
| return new PagedResponse<RuleSummaryDto> | |
| { Items = items, TotalCount = total, Page = query.Page, PageSize = query.PageSize }; | |
| } | |
| public async Task<Rule?> GetByIdAsync(Guid id, Guid tenantId, CancellationToken ct = default) | |
| => await _db.Rules.IgnoreQueryFilters() | |
| .Include(r => r.CurrentVersion) | |
| .Include(r => r.Scopes) | |
| .Include(r => r.Category) | |
| .FirstOrDefaultAsync(r => r.Id == id && r.TenantId == tenantId, ct); | |
| public async Task<Rule> CreateWithVersionAsync(Rule rule, RuleVersion version, CancellationToken ct = default) | |
| { | |
| _db.Rules.Add(rule); | |
| await _db.SaveChangesAsync(ct); | |
| version.RuleId = rule.Id; | |
| version.TenantId = rule.TenantId; | |
| _db.RuleVersions.Add(version); | |
| await _db.SaveChangesAsync(ct); | |
| rule.CurrentVersionId = version.Id; | |
| await _db.SaveChangesAsync(ct); | |
| return rule; | |
| } | |
| public async Task UpdateWithNewVersionAsync(Rule rule, RuleVersion newVersion, CancellationToken ct = default) | |
| { | |
| _db.Rules.Update(rule); | |
| _db.RuleVersions.Add(newVersion); | |
| await _db.SaveChangesAsync(ct); | |
| } | |
| public async Task PublishAsync(Guid id, Guid tenantId, Guid publishedBy, CancellationToken ct = default) | |
| { | |
| var rule = await _db.Rules.IgnoreQueryFilters().Include(r => r.CurrentVersion) | |
| .FirstOrDefaultAsync(r => r.Id == id && r.TenantId == tenantId, ct); | |
| if (rule == null) return; | |
| rule.IsPublished = true; | |
| rule.IsDraft = false; | |
| rule.Status = RuleStatus.Published; | |
| if (rule.CurrentVersion != null) | |
| { | |
| rule.CurrentVersion.Status = RuleStatus.Published; | |
| rule.CurrentVersion.PublishedBy = publishedBy; | |
| rule.CurrentVersion.PublishedAt = DateTime.UtcNow; | |
| } | |
| await _db.SaveChangesAsync(ct); | |
| } | |
| public async Task<Rule> CloneAsync(Guid id, Guid tenantId, Guid clonedBy, string? newCode, string? newName, CancellationToken ct = default) | |
| { | |
| var original = await GetByIdAsync(id, tenantId, ct); | |
| var clone = new Rule | |
| { | |
| TenantId = tenantId, | |
| RuleCode = newCode ?? $"{original!.RuleCode}_copy_{DateTime.UtcNow.Ticks}", | |
| RuleName = newName ?? $"{original.RuleName} (Copy)", | |
| Description = original.Description, | |
| CategoryId = original.CategoryId, | |
| RuleType = original.RuleType, | |
| Priority = original.Priority, | |
| Tags = new List<string>(original.Tags), | |
| Status = RuleStatus.Draft, | |
| CreatedBy = clonedBy | |
| }; | |
| var version = new RuleVersion | |
| { | |
| TenantId = tenantId, | |
| VersionNumber = 1, | |
| VersionLabel = "v1.0", | |
| RuleDefinition = original!.CurrentVersion!.RuleDefinition, | |
| ChangeSummary = $"Cloned from {original.RuleCode}", | |
| IsCurrent = true, | |
| Status = RuleStatus.Draft, | |
| CreatedBy = clonedBy | |
| }; | |
| return await CreateWithVersionAsync(clone, version, ct); | |
| } | |
| public async Task ToggleActiveAsync(Guid id, Guid tenantId, CancellationToken ct = default) | |
| { | |
| var rule = await _db.Rules.IgnoreQueryFilters() | |
| .FirstOrDefaultAsync(r => r.Id == id && r.TenantId == tenantId, ct); | |
| if (rule == null) return; | |
| rule.IsActive = !rule.IsActive; | |
| await _db.SaveChangesAsync(ct); | |
| } | |
| public async Task UpdateScopesAsync(Guid id, Guid tenantId, List<RuleScopeRequest> scopes, CancellationToken ct = default) | |
| { | |
| var existing = _db.RuleScopes.Where(s => s.RuleId == id); | |
| _db.RuleScopes.RemoveRange(existing); | |
| foreach (var s in scopes) | |
| { | |
| _db.RuleScopes.Add(new RuleScope | |
| { | |
| RuleId = id, TenantId = tenantId, | |
| ScopeType = Enum.Parse<ScopeType>(s.ScopeType, true), | |
| ScopeValue = s.ScopeValue, IsExcluded = s.IsExcluded | |
| }); | |
| } | |
| await _db.SaveChangesAsync(ct); | |
| } | |
| } | |
| // ---- RuleVersionRepository ---- | |
| public class RuleVersionRepository : IRuleVersionRepository | |
| { | |
| private readonly BREDbContext _db; | |
| public RuleVersionRepository(BREDbContext db) => _db = db; | |
| public async Task<RuleVersion?> GetLatestAsync(Guid ruleId, CancellationToken ct = default) | |
| => await _db.RuleVersions.Where(v => v.RuleId == ruleId) | |
| .OrderByDescending(v => v.VersionNumber).FirstOrDefaultAsync(ct); | |
| public async Task<List<RuleVersion>> GetAllAsync(Guid ruleId, Guid tenantId, CancellationToken ct = default) | |
| => await _db.RuleVersions | |
| .Where(v => v.RuleId == ruleId && v.TenantId == tenantId) | |
| .OrderByDescending(v => v.VersionNumber).ToListAsync(ct); | |
| } | |
| // ---- RuleApprovalService ---- | |
| public class RuleApprovalService : IRuleApprovalService | |
| { | |
| private readonly BREDbContext _db; | |
| public RuleApprovalService(BREDbContext db) => _db = db; | |
| public async Task SubmitForApprovalAsync(Guid ruleId, Guid versionId, Guid tenantId, Guid requestedBy, string? comments, CancellationToken ct = default) | |
| { | |
| await _db.Rules.IgnoreQueryFilters().Where(r => r.Id == ruleId) | |
| .ExecuteUpdateAsync(s => s.SetProperty(r => r.Status, RuleStatus.PendingApproval), ct); | |
| _db.RuleApprovals.Add(new RuleApproval | |
| { | |
| RuleId = ruleId, VersionId = versionId, TenantId = tenantId, | |
| RequestedBy = requestedBy, Comments = comments, Status = "PENDING" | |
| }); | |
| await _db.SaveChangesAsync(ct); | |
| } | |
| public async Task ApproveAsync(Guid ruleId, Guid tenantId, Guid approvedBy, string? comments, CancellationToken ct = default) | |
| { | |
| await _db.Rules.IgnoreQueryFilters().Where(r => r.Id == ruleId) | |
| .ExecuteUpdateAsync(s => s.SetProperty(r => r.Status, RuleStatus.Approved), ct); | |
| var approval = await _db.RuleApprovals.OrderByDescending(a => a.RequestedAt) | |
| .FirstOrDefaultAsync(a => a.RuleId == ruleId && a.Status == "PENDING", ct); | |
| if (approval != null) | |
| { | |
| approval.Status = "APPROVED"; | |
| approval.ReviewedBy = approvedBy; | |
| approval.ReviewedAt = DateTime.UtcNow; | |
| approval.Comments = comments; | |
| await _db.SaveChangesAsync(ct); | |
| } | |
| } | |
| public async Task RejectAsync(Guid ruleId, Guid tenantId, Guid rejectedBy, string comments, CancellationToken ct = default) | |
| { | |
| await _db.Rules.IgnoreQueryFilters().Where(r => r.Id == ruleId) | |
| .ExecuteUpdateAsync(s => s.SetProperty(r => r.Status, RuleStatus.Draft), ct); | |
| var approval = await _db.RuleApprovals.OrderByDescending(a => a.RequestedAt) | |
| .FirstOrDefaultAsync(a => a.RuleId == ruleId && a.Status == "PENDING", ct); | |
| if (approval != null) | |
| { | |
| approval.Status = "REJECTED"; | |
| approval.ReviewedBy = rejectedBy; | |
| approval.ReviewedAt = DateTime.UtcNow; | |
| approval.Comments = comments; | |
| await _db.SaveChangesAsync(ct); | |
| } | |
| } | |
| } | |
| // ---- AuditService ---- | |
| public class AuditService : IAuditService | |
| { | |
| private readonly BREDbContext _db; | |
| public AuditService(BREDbContext db) => _db = db; | |
| public async Task LogAsync(AuditLog log, CancellationToken ct = default) | |
| { | |
| _db.AuditLogs.Add(log); | |
| await _db.SaveChangesAsync(ct); | |
| } | |
| } | |
| // Placeholder entity for AI prompts (extend BREDbContext if needed) | |
| public class AiPromptEntity | |
| { | |
| public Guid Id { get; set; } | |
| public string PromptCode { get; set; } = default!; | |
| public string PromptTemplate { get; set; } = default!; | |
| public bool IsActive { get; set; } = true; | |
| } | |