using PostFundManagement.Application.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; 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.EvidenceBucketName)] IS3Service s3Service) : IService { public async ValueTask UploadEvidenceFileAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) { try { _ = 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); using var stream = file.OpenReadStream(); var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken); if (uploadResult.IsFailed) return Result.Fail(uploadResult.Errors.FirstOrDefault()?.Message ?? "File upload failed."); var documentUrl = uploadResult.Value; 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 = documentUrl, Status = ApprovalStatus.Pending }; context.Evidences.Add(evidence); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Evidence upload failed"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) { try { var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash); if (decodeResult.IsFailed) return Result.Fail(decodeResult.Errors); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var totalUpdated = await context.Indicators.Where(i => i.Id == decodeResult.Value) .ExecuteUpdateAsync(f => f .SetProperty(f => f.ActualAmount, amount) .SetProperty(f => f.CreatedBy, userId), cancellationToken); return totalUpdated > 0 ? Result.Ok() : Result.Fail("Failed to record actual amount"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetMilestonesByProjectIdAsync(string projectIdHash, CancellationToken cancellationToken = default) { try { var decodeResult = hashService.DecodeLongIdHash(projectIdHash); if (decodeResult.IsFailed) return Result.Fail(decodeResult.Errors); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var milestones = await context.Milestones.AsNoTracking() .Where(m => m.ProjectId == decodeResult.Value) .OrderBy(m => m.DueAt) .ToListAsync(cancellationToken); return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var milestones = await context.Milestones.AsNoTracking() .OrderByDescending(m => m.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(cancellationToken); return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) { try { 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() ?? []); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) { try { 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() ?? []); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail($"Project with ID {request.ProjectId} does not exist."); 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 await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Failed to create the milestone"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Users.AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken)) return Result.Fail("Invalid user"); if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) return Result.Fail($"Project with ID {request.ProjectId} does not exist."); 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 await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Failed to create the invite"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async Task> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) { try { var decodeResult = hashService.DecodeLongIdHash(indicatorId); if (decodeResult.IsFailed) return Result.Fail(decodeResult.Errors); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var indicator = await context.Indicators.AsNoTracking().FirstOrDefaultAsync(i => i.Id == decodeResult.Value, cancellationToken); return indicator is not null ? Result.Ok(indicator.Map()) : Result.Fail("Failed to find specific indicator."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async Task> GetMilestoneAsync(string milestoneId, CancellationToken cancellationToken) { try { var decodeResult = hashService.DecodeLongIdHash(milestoneId); if (decodeResult.IsFailed) return Result.Fail(decodeResult.Errors); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var milestone = await context.Milestones.AsNoTracking().FirstOrDefaultAsync(m => m.Id == decodeResult.Value, cancellationToken); return milestone is not null ? Result.Ok(milestone.Map()) : Result.Fail("Failed to get specific milestone"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) { try { var decodeResult = hashService.DecodeLongIdHash(evidenceId); if (decodeResult.IsFailed) return Result.Fail(decodeResult.Errors); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var evidence = await context.Evidences.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken); return evidence is not null ? Result.Ok(evidence.Map()) : Result.Fail("Failed to get a specific evidence record."); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } }