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.EvidenceBucketName)] IS3Service s3Service) : IEvidenceService { public async ValueTask 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, stream, file.ContentType, cancellationToken); if (uploadResult.IsFailed) return Result.Fail(uploadResult.Errors); var documentUrl = uploadResult.Value; 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 = documentUrl, Status = ApprovalStatus.Pending }; context.Evidences.Add(evidence); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); })); public async ValueTask 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.New); })); public async ValueTask> 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 ValueTask> 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 ValueTask 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 ValueTask 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.New); })); public async ValueTask> 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.New) .ToResult(indicator!.Map()); })); public async ValueTask> 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.New) .ToResult(evidence!.Map()); })); public async ValueTask> 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 ValueTask> 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 ValueTask 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.New); })); }