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.Evidence; public sealed class EvidenceService(IDbContextFactory contextFactory, HashService hashService, [FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service) : IEvidenceService { public async Task CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { if (!long.TryParse(form["projectId"], out var projectId) || !long.TryParse(form["milestoneId"], out var milestoneId) || !long.TryParse(form["indicatorId"], out var indicatorId) || !long.TryParse(form["activityId"], out var activityId) || !Enum.TryParse(form["type"], out var type)) return Result.Fail(CreateError.Create("Invalid or missing form data parameters.")); await using var stream = file.OpenReadStream(); var uploadResult = await s3Service.UploadFileAsync(file.FileName, "evidence", stream, file.ContentType, cancellationToken); if (uploadResult.IsFailed) return Result.Fail(uploadResult.Errors); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var evidence = new Domain.Entities.Evidence { ProjectId = projectId, MilestoneId = milestoneId, IndicatorId = indicatorId, ActivityId = activityId, CreatedBy = userId, CreatedAt = DateTime.UtcNow, Version = 1, Type = type, DocumentUrl = uploadResult.Value, Status = ApprovalStatus.Pending }; context.Evidences.Add(evidence); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create evidence.")); })); public async Task RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var changedTotal = await context.Indicators.Where(i => i.Id == decodeResult.Value) .ExecuteUpdateAsync(f => f .SetProperty(f => f.ActualAmount, amount) .SetProperty(f => f.CreatedBy, userId), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update indicator.")); })); public async Task> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var indicators = await context.Indicators.AsNoTracking() .OrderByDescending(i => i.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(indicators?.Select(i => i.Map()).ToArray() ?? []); })); public async Task> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var evidence = await context.Evidences.AsNoTracking() .OrderByDescending(e => e.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(evidence?.Select(i => i.Map()).ToArray() ?? []); })); public async Task CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) return Result.Fail(NotFoundError.New); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail(NotFoundError.New); context.Milestones.Add(new Domain.Entities.Milestone { ProjectId = request.ProjectId, DueAt = request.DueAt, Name = request.Name, Priority = request.Priority, Status = ApprovalStatus.Pending, CreatedBy = userId, CreatedAt = DateTime.UtcNow }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("No record could be created.")); })); public async Task CreateIndicatorAsync(CreateIndicatorRequest 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.Indicators.Add(new Domain.Entities.Indicator { ProjectId = request.ProjectId, Name = request.Name, UnitOfMeasure = request.UnitOfMeasure, BaselineAmount = request.BaselineAmount, TargetAmount = request.TargetAmount, ActualAmount = request.BaselineAmount, CreatedBy = userId, CreatedAt = DateTime.UtcNow }); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create indicator.")); })); public async Task> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(indicatorId); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var indicator = await context.Indicators.AsNoTracking().FirstOrDefaultAsync(i => i.Id == decodeResult.Value, cancellationToken); return Result.FailIf(indicator is null, NotFoundError.Create("Indicator not found.")) .ToResult(indicator?.Map()!); })); public async Task> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(evidenceId); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var evidence = await context.Evidences.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken); return Result.FailIf(evidence is null, NotFoundError.Create("Evidence not found.")) .ToResult(evidence?.Map()!); })); public async Task> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var evidences = await context.Evidences.AsNoTracking() .Where(e => e.MilestoneId == decodeResult.Value) .OrderByDescending(o => o.CreatedAt) .ThenByDescending(o => o.UpdatedAt) .ToListAsync(cancellationToken); return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); })); public async Task> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(projectIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var evidences = await context.Evidences.AsNoTracking() .Where(e => e.ProjectId == decodeResult.Value) .OrderByDescending(o => o.CreatedAt) .ThenBy(o => o.UpdatedAt) .ToListAsync(cancellationToken); return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); })); public async Task UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Evidences.AnyAsync(e => e.Id == request.EvidenceId, cancellationToken)) return Result.Fail(NotFoundError.New); var changedTotal = await context.Evidences.Where(e => e.Id == request.EvidenceId) .ExecuteUpdateAsync(f => f .SetProperty(f => f.Version, request.Version) .SetProperty(f => f.Type, request.EvidenceType) .SetProperty(f => f.Status, request.Status) .SetProperty(f => f.UpdatedBy, userId) .SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken); return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update evidence.")); })); public async Task> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(evidenceIdHash); if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create() .CausedBy(decodeResult.Errors)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var evidence = await context.Evidences.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken); if (evidence is null) return Result.Fail(NotFoundError.New); if (string.IsNullOrWhiteSpace(evidence.DocumentUrl)) return Result.Fail(ValidationError.Create("Ecidence is invalid, has no saved documentUrl")); var secureDownloadUrlResult = await s3Service.GeneratePresignedUrlAsync(evidence.DocumentUrl, TimeSpan.FromMinutes(15), null, cancellationToken); return Result.OkIf(secureDownloadUrlResult.IsSuccess && !string.IsNullOrWhiteSpace(secureDownloadUrlResult.Value), NotFoundError.Create("Failed to generate secure file url")) .ToResult(secureDownloadUrlResult.Value); })); }