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.Evidence; public sealed class EvidenceService(IDbContextFactory contextFactory, HashService hashService, [FromKeyedServices(Constants.EvidenceS3SettingsSection)] 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 hash, decimal amount, ClaimsPrincipal principal, CancellationToken cancellationToken = default) { try { var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; if (!Guid.TryParse(sidClaim, out var userId)) return Result.Fail("Missing or invalid 'sid' claim."); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var hashResult = hashService.DecodeLongIdHash(hash); long indicatorId = 0; if (hashResult.IsFailed) return Result.Fail(hashResult.Errors); var totalUpdated = await context.Indicators.Where(i => i.Id == indicatorId) .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(long projectId, CancellationToken cancellationToken = default) { try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var milestones = await context.Milestones.AsNoTracking() .Where(m => m.ProjectId == projectId) .OrderBy(m => m.DueAt) .ToListAsync(cancellationToken); return Result.Ok(milestones.Select(m => m.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 milestones?.Count > 0 ? Result.Ok(milestones.Select(i => i.Map()).ToArray()) : Result.Ok([]); } 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 indicators?.Count > 0 ? Result.Ok(indicators.Select(i => i.Map()).ToArray()) : Result.Ok([]); } 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 evidence?.Count > 0 ? Result.Ok(evidence.Select(e => e.Map()).ToArray()) : Result.Ok([]); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> CreateMilestoneAsync(CreateMilestoneRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken = default) { try { var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; if (!Guid.TryParse(sidClaim, out var userId)) return Result.Fail("Missing or invalid 'sid' claim."); 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."); var milestone = new Domain.Entities.Milestone { ProjectId = request.ProjectId, DueAt = request.DueAt, Name = request.Name, Priority = request.Priority, Status = ApprovalStatus.Pending, CreatedBy = userId, CreatedAt = DateTime.UtcNow }; context.Milestones.Add(milestone); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok((hashService.HashEncodeLongId(milestone.Id).Value, milestone.Map())) : 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, ClaimsPrincipal principal, CancellationToken cancellationToken = default) { try { var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value; if (!Guid.TryParse(sidClaim, out var userId)) return Result.Fail("Missing or invalid 'sid' claim."); 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."); var indicator = 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 }; context.Indicators.Add(indicator); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok((hashService.HashEncodeLongId(indicator.Id).Value, indicator.Map())) : Result.Fail("Failed to create the invite"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } }