using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Execution; public sealed class ExecutionService(IDbContextFactory contextFactory, HashService hashService, ITenantContextService tenantContext) : IExecutionService { public async Task> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var risks = await context.Risks.AsNoTracking() .OrderByDescending(r => r.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(risks?.Select(i => i.Map()).ToArray() ?? []); })); public async Task> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var disbursements = await context.Disbursements.AsNoTracking() .OrderByDescending(d => d.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToArrayAsync(cancellationToken); return Result.Ok(disbursements?.Select(i => i.Map()).ToArray() ?? []); })); public async Task CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(NotFoundError.New); context.Risks.Add(new Domain.Entities.Risk { ProjectId = request.ProjectId, Likelihood = request.Likelihood, Impact = request.Impact, Name = request.Name, Description = request.Description, CreatedBy = userId, CreatedAt = DateTime.UtcNow }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); })); public async Task CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(NotFoundError.New); context.Disbursements.Add(new Domain.Entities.Disbursement { ProjectId = request.ProjectId, MilestoneId = request.MilestoneId, Amount = request.Amount, Status = ApprovalStatus.Pending, CreatedBy = userId, CreatedAt = DateTime.UtcNow }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); })); public async Task ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var changedTotal = await context.Disbursements.Where(d => d.Id == decodeResult.Value) .ExecuteUpdateAsync(f => f .SetProperty(f => f.Status, ApprovalStatus.Approved) .SetProperty(f => f.UpdatedBy, userId) .SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.New); })); public async Task> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var disbursement = await context.Disbursements.AsNoTracking() .FirstOrDefaultAsync(d => d.Id == decodeResult.Value, cancellationToken); return Result.FailIf(disbursement is null, NotFoundError.New) .ToResult(disbursement!.Map()); })); public async Task> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(riskIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var risk = await context.Risks.AsNoTracking() .FirstOrDefaultAsync(r => r.Id == decodeResult.Value, cancellationToken); return Result.FailIf(risk is null, NotFoundError.New) .ToResult(risk!.Map()); })); public async Task UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var changedTotal = await context.Risks .Where(r => r.Id == request.RiskId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Name, request.Name) .SetProperty(u => u.Description, request.Description) .SetProperty(u => u.Likelihood, request.Likelihood) .SetProperty(u => u.Impact, request.Impact), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.New); })); public async Task UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var changedTotal = await context.Disbursements .Where(f => f.Id == request.DisbursementId) .ExecuteUpdateAsync(f => f .SetProperty(d => d.MilestoneId, request.MilestoneId) .SetProperty(d => d.Amount, request.Amount) .SetProperty(d => d.UpdatedAt, DateTime.UtcNow) .SetProperty(d => d.UpdatedBy, userId), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.New); })); public async Task CreateRuleAsync(CreateRuleRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken)) return Result.Fail(ValidationError.Create("Programme does not exist")); var requestedName = request.Name.Humanize(LetterCasing.Title).Trim(); if (await context.Rules.AnyAsync(r => r.Name == requestedName && r.ProgrammeId == request.ProgrammeId, cancellationToken)) return Result.Fail(ValidationError.Create("A rule with the same name exists")); context.Rules.Add(new Domain.Entities.Rule { CreatedAt = DateTime.UtcNow, CreatedBy = userId, Description = request.Description, Name = requestedName, ProgrammeId = request.ProgrammeId, Status = ActivityStatus.Active, Version = 1, TenantId = tenantContext.TenantId }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create rule")); })); public async Task UpdateRuleAsync(UpdateRuleRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var requestedName = request.Name.Humanize(LetterCasing.Title).Trim(); var changedTotal = await context.Rules .Where(f => f.Id == request.RuleId) .ExecuteUpdateAsync(f => f .SetProperty(u => u.Status, request.Status) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow) .SetProperty(u => u.Name, requestedName) .SetProperty(u => u.Version, request.Version + 1) .SetProperty(u => u.Description, request.Description), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update rule")); })); public async Task> GetRuleAsync(string ruleIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(ruleIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rule = await context.Rules.AsNoTracking().FirstOrDefaultAsync(r => r.Id == decodeResult.Value, cancellationToken); return Result.FailIf(rule is null, NotFoundError.New) .ToResult(rule!.Map()); })); public async Task> GetRulesAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var rules = await context.Rules.AsNoTracking() .OrderByDescending(d => d.Id) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToArrayAsync(cancellationToken); return Result.Ok(rules?.Select(i => i.Map()).ToArray() ?? []); })); }