// ============================================================ // 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 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> 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> GetWeightsAsync(Guid tenantId, string? productCode = null) => Task.FromResult(new Dictionary { ["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 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 GetByRequestIdAsync(Guid requestId, Guid tenantId, CancellationToken ct = default) => await _db.ExecutionResults.FirstOrDefaultAsync(r => r.RequestId == requestId && r.TenantId == tenantId, ct); public async Task> 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 { 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 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 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 GetPromptAsync(string promptCode, CancellationToken ct = default) { var p = await _db.Set() .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 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 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> 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> 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> 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> GetPagedAsync(RuleQuery query, CancellationToken ct = default) { var q = _db.Rules.Where(r => r.TenantId == query.TenantId).IgnoreQueryFilters(); if (query.Status != null && Enum.TryParse(query.Status, true, out var s)) q = q.Where(r => r.Status == s); if (query.RuleType != null && Enum.TryParse(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 { Items = items, TotalCount = total, Page = query.Page, PageSize = query.PageSize }; } public async Task 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 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 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(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 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(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 GetLatestAsync(Guid ruleId, CancellationToken ct = default) => await _db.RuleVersions.Where(v => v.RuleId == ruleId) .OrderByDescending(v => v.VersionNumber).FirstOrDefaultAsync(ct); public async Task> 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; }