Refactored endpoints to be more functional and more expressive

This commit is contained in:
2026-09-01 11:23:25 +02:00
parent 346cb8da5f
commit 2df5b5c2ed
86 changed files with 1565 additions and 1575 deletions
@@ -1,35 +1,34 @@
using PostFundManagement.Application.Shared;
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<ApplicationDbContext> contextFactory, HashService hashService,
public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
[FromKeyedServices(Constants.EvidenceBucketName)] IS3Service s3Service) : IService
{
public async ValueTask<Result> 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<EvidenceType>(form["type"], out var type);
public async ValueTask<Result> UploadEvidenceFileAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(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<EvidenceType>(form["type"], out var type))
return Result.Fail(CreateError.Create("Invalid or missing form data parameters."));
using var stream = file.OpenReadStream();
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.FirstOrDefault()?.Message ?? "File upload failed.");
return Result.Fail(uploadResult.Errors);
var documentUrl = uploadResult.Value;
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var evidence = new Domain.Entities.Evidence
{
@@ -47,47 +46,32 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
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));
}
}
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
}));
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default)
{
try
{
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var totalUpdated = await context.Indicators.Where(i => i.Id == decodeResult.Value)
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 totalUpdated > 0
? Result.Ok()
: Result.Fail("Failed to record actual amount");
}
catch (Exception ex)
return Result.FailIf(changedTotal <= 0, UpdateError.New);
}));
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var indicators = await context.Indicators.AsNoTracking()
.OrderByDescending(i => i.CreatedAt)
@@ -95,19 +79,13 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.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));
}
}
return Result.Ok(indicators.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var evidence = await context.Evidences.AsNoTracking()
.OrderByDescending(e => e.CreatedAt)
@@ -115,22 +93,16 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.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));
}
}
return Result.Ok(evidence.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await 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.");
return Result.Fail(NotFoundError.New);
context.Milestones.Add(new Domain.Entities.Milestone
{
@@ -143,27 +115,16 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
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));
}
}
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
}));
public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
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");
await 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.");
return Result.Fail(NotFoundError.New);
context.Indicators.Add(new Domain.Entities.Indicator
{
@@ -177,72 +138,55 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
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));
}
}
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
}));
public async Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken)
{
try
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(indicatorId);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await 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 ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default)
{
try
: Result.Fail(NotFoundError.New);
}));
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(evidenceId);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Domain.Models.Evidence>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await 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));
}
}
: Result.Fail<Domain.Models.Evidence>(NotFoundError.New);
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var evidences = await context.Evidences.AsNoTracking()
.Where(e => e.MilestoneId == decodeResult.Value)
@@ -250,24 +194,19 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.ThenByDescending(o => o.UpdatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(evidences.Select(e => e.Map()).ToArray());
}));
public async Task<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var evidences = await context.Evidences.AsNoTracking()
.Where(e => e.ProjectId == decodeResult.Value)
@@ -275,11 +214,6 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.ThenBy(o => o.UpdatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(evidences.Select(e => e.Map()).ToArray());
}));
}