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,5 +1,6 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database;
@@ -8,11 +9,10 @@ namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
{
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var invite = new Domain.Entities.Invite
{
@@ -40,19 +40,13 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(invite.Map())
: Result.Fail("Failed to complete the insite distribution");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail<Invite>(CreateError.New);
}));
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var invites = await context.Invites.AsNoTracking()
.OrderByDescending(i => i.CreatedAt)
@@ -60,29 +54,18 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(invites?.Select(i => i.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(invites.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken)
{
try
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var invite = await context.Invites.AsNoTracking().FirstOrDefaultAsync(i => i.Recipient == userId, cancellationToken);
return invite is not null
? Result.Ok(invite.Map())
: Result.Fail("Failed to find specific invite.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail<Invite>(NotFoundError.New);
}));
}
@@ -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());
}));
}
@@ -1,6 +1,7 @@
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;
@@ -9,11 +10,10 @@ namespace PostFundManagement.Application.Services.Execution;
public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
{
public async Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var risks = await context.Risks.AsNoTracking()
.OrderByDescending(r => r.CreatedAt)
@@ -21,66 +21,30 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(risks?.Select(i => i.Map()).ToArray() ?? []);
}
catch (Exception ex)
return Result.Ok(risks.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement[]>>>)(async () =>
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(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 risks = await context.Risks.AsNoTracking()
.Where(r => r.ProjectId == decodeResult.Value)
.OrderByDescending(r => r.CreatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(risks?.Select(i => i.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var disbursements = await context.Disbursements.AsNoTracking()
.OrderByDescending(d => d.CreatedAt)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
.ToArrayAsync(cancellationToken);
return Result.Ok(disbursements?.Select(i => i.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(disbursements.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> CreateRiskAsync(CreateRiskRequest 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.Risks.Add(new Domain.Entities.Risk
{
@@ -93,24 +57,16 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
CreatedAt = DateTime.UtcNow
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to create project risk");
}
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> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest 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.Disbursements.Add(new Domain.Entities.Disbursement
{
@@ -122,40 +78,96 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
CreatedAt = DateTime.UtcNow
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to create disbursement");
}
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> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default)
{
try
{
public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash);
if (decodeResult.IsFailed)
return Result.Fail("Invalid disbursementId");
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var totalSaved = await context.Disbursements.Where(d => d.Id == decodeResult.Value)
var changedTotal = await context.Disbursements.Where(d => d.Id == decodeResult.Value)
.ExecuteUpdateAsync(f =>
f.SetProperty(f => f.Status, ApprovalStatus.Approved)
.SetProperty(f => f.UpdatedBy, userId)
.SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken);
return totalSaved > 0
? Result.Ok()
: Result.Fail("Failed to approve payment");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.FailIf(changedTotal <= 0, UpdateError.New);
}));
public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Disbursement>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Disbursement>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var disbursement = await context.Disbursements.AsNoTracking()
.FirstOrDefaultAsync(d => d.Id == decodeResult.Value, cancellationToken);
return disbursement is not null
? Result.Ok(disbursement.Map())
: Result.Fail<Disbursement>(NotFoundError.New);
}));
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Risk>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(riskIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Risk>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var risk = await context.Risks.AsNoTracking()
.FirstOrDefaultAsync(r => r.Id == decodeResult.Value, cancellationToken);
return risk is not null
? Result.Ok(risk.Map())
: Result.Fail<Risk>(NotFoundError.New);
}));
public async ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Risks
.Where(r => r.Id == request.RiskId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Name, request.Name)
.SetProperty(u => u.Description, request.Description)
.SetProperty(u => u.Likelihood, request.Likelihood)
.SetProperty(u => u.Impact, request.Impact), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
}));
public async ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Disbursements
.Where(f => f.Id == request.DisbursementId)
.ExecuteUpdateAsync(f => f
.SetProperty(d => d.MilestoneId, request.MilestoneId)
.SetProperty(d => d.Amount, request.Amount)
.SetProperty(d => d.UpdatedAt, DateTime.UtcNow)
.SetProperty(d => d.UpdatedBy, userId), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
}));
}
@@ -2,6 +2,10 @@
namespace PostFundManagement.Application.Services.Execution;
public record UpdateDisbursementRequest(long DisbursementId, long? MilestoneId, decimal Amount);
public record UpdateRiskRequest(long RiskId, RiskLikelihood Likelihood, RiskImpact Impact, string Name, string Description);
public record CreateDisbursementRequest(long ProjectId, long? MilestoneId, decimal Amount);
public record CreateRiskRequest(long ProjectId, RiskLikelihood Likelihood, RiskImpact Impact, string Name, string Description);
@@ -1,6 +1,7 @@
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;
@@ -9,43 +10,30 @@ namespace PostFundManagement.Application.Services.Governance;
public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
{
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var programmes = await context.Programmes.AsNoTracking().ToListAsync(cancellationToken);
return Result.Ok(programmes?.Select(p => p.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(programmes.Select(p => p.Map()).ToArray());
}));
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var portfolios = await context.Portfolios.AsNoTracking().ToListAsync(cancellationToken);
return Result.Ok(portfolios?.Select(p => p.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(portfolios.Select(p => p.Map()).ToArray());
}));
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var organisations = await context.Organisations.AsNoTracking()
.OrderBy(o => o.Name)
@@ -53,59 +41,42 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(organisations?.Select(o => o.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(organisations.Select(o => o.Map()).ToArray());
}));
public async ValueTask<Result<Organisation>> GetOrganisationByIdAsync(string organisationIdHash, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Organisation>> GetOrganisationByIdAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(organisationIdHash);
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 organisation = await context.Organisations.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken);
return organisation is not null
? Result.Ok(organisation.Map())
: Result.Fail("Failed to find specific organisation");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail(NotFoundError.New);
}));
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var instruments = await context.Instruments.AsNoTracking().ToListAsync(cancellationToken);
return Result.Ok(instruments?.Select(i => i.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(instruments.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Dashboard>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var totalPortfolioValue = await context.Awards.SumAsync(a => a.Amount, cancellationToken);
var activeAwardsCount = await context.Awards.CountAsync(a => a.Status == ApprovalStatus.Approved, cancellationToken);
@@ -126,21 +97,15 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.Ok(new Dashboard(totalPortfolioValue, activeAwardsCount,
disbursedAmount, onTrackPercentage, atRiskCount, beneficiariesReached));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
}));
public async ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public async ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Organisations.AnyAsync(o => o.RegistrationNo == request.RegistrationNo, cancellationToken))
return Result.Fail("Organisation with the same registration number already exists");
return Result.Fail(CreateError.Create("Organisation with the same registration number already exists"));
context.Organisations.Add(new Domain.Entities.Organisation
{
@@ -153,13 +118,6 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
CreatedAt = DateTime.UtcNow
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to create organisation");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
}));
}
@@ -1,6 +1,7 @@
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;
@@ -9,11 +10,10 @@ namespace PostFundManagement.Application.Services.Grants;
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
{
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contracts = await context.Contracts.AsNoTracking()
.OrderByDescending(c => c.CreatedAt)
@@ -21,42 +21,32 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(contracts?.Select(c => c.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(contracts.Select(c => c.Map()).ToArray());
}));
public async ValueTask<Result<Contract>> GetContractByIdAsync(string contractIdHash, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Contract>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contract = await context.Contracts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken);
var contract = await context.Contracts.AsNoTracking()
.FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken);
return contract is not null
? Result.Ok(contract.Map())
: Result.Fail("Failed to find specific contract");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail<Contract>(NotFoundError.New);
}));
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var budgets = await context.Budgets.AsNoTracking()
.OrderByDescending(b => b.CreatedAt)
@@ -64,42 +54,32 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(budgets?.Select(b => b.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(budgets.Select(b => b.Map()).ToArray());
}));
public async ValueTask<Result<Budget>> GetBudgetByIdAsync(string budgetIdHash, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(budgetIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Budget>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var budget = await context.Budgets.AsNoTracking().FirstOrDefaultAsync(b => b.Id == decodeResult.Value, cancellationToken);
var budget = await context.Budgets.AsNoTracking()
.FirstOrDefaultAsync(b => b.Id == decodeResult.Value, cancellationToken);
return budget is not null
? Result.Ok(budget.Map())
: Result.Fail("Failed to get specific budget.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail<Budget>(NotFoundError.New);
}));
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var awards = await context.Awards.AsNoTracking()
.OrderByDescending(a => a.CreatedAt)
@@ -107,42 +87,31 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(awards?.Select(a => a.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(awards.Select(a => a.Map()).ToArray());
}));
public async ValueTask<Result<Award>> GetAwardByIdAsync(string awardIdHash, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(awardIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Award>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var award = await context.Awards.AsNoTracking().FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken);
return award is not null
? Result.Ok(award.Map())
: Result.Fail("Award not found");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail<Award>(NotFoundError.New);
}));
public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest 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);
var changedTotal = await context.Contracts
.Where(c => c.Id == request.ContractId && c.Status != ContractStatus.Active)
@@ -152,27 +121,19 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.SetProperty(u => u.CreatedBy, userId)
.SetProperty(u => u.Status, ContractStatus.Active), cancellationToken);
return changedTotal > 0
? Result.Ok()
: Result.Fail("Contract activation failed");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, UpdateError.New);
}));
public async ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> CreateContractAsync(CreateContractRequest 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);
var award = await context.Awards.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == request.AwardId, cancellationToken);
if (award == null)
return Result.Fail($"Award with ID {request.AwardId} does not exist.");
return Result.Fail(NotFoundError.New);
var contract = new Domain.Entities.Contract
{
@@ -187,27 +148,19 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
context.Contracts.Add(contract);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to create contract");
}
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> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
if (request.Amount <= 0)
return Result.Fail("Budget amount must be greater than zero.");
return Result.Fail(ValidationError.Create("Budget amount must be greater than zero."));
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);
var budget = new Domain.Entities.Budget
{
@@ -220,30 +173,22 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
context.Budgets.Add(budget);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to create budget");
}
catch (Exception ex)
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
}));
public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
{
if (request.Amount <= 0)
return Result.Fail("Approved amount must be greater than zero.");
return Result.Fail(ValidationError.Create("Approved amount must be greater than zero."));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Organisations.AnyAsync(o => o.Id == request.OrganisationId, cancellationToken))
return Result.Fail($"Organisation with ID {request.OrganisationId} does not exist.");
return Result.Fail(NotFoundError.New);
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
return Result.Fail($"Programme with ID {request.ProgrammeId} does not exist.");
return Result.Fail(NotFoundError.New);
var award = new Domain.Entities.Award
{
@@ -257,13 +202,6 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
context.Awards.Add(award);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to create award.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
}));
}
@@ -1,6 +1,7 @@
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;
@@ -9,16 +10,15 @@ namespace PostFundManagement.Application.Services.Projects;
public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
{
public async ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest 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);
var project = await context.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, cancellationToken);
if (project == null)
return Result.Fail("Project does not exist.");
return Result.Fail(NotFoundError.New);
project.Status = request.Status;
project.UpdatedBy = userId;
@@ -33,93 +33,69 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.SetProperty(p => p.UpdatedBy, userId)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
return changedTotal > 0
? Result.Ok()
: Result.Fail("Failed to update project");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.FailIf(changedTotal == 0, UpdateError.New);
}));
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var projects = await context.Projects.AsNoTracking().OrderByDescending(p => p.CreatedAt)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(projects?.Select(p => p.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(projects.Select(p => p.Map()).ToArray());
}));
public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Project>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var project = await context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken);
var project = await context.Projects.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken);
return project is not null
? Result.Ok(project.Map())
: Result.Fail("Failed to get specific project.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail<Project>(NotFoundError.Create());
}));
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Domain.Models.Activity[]>(IdDecodeError.Create().CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var activities = await context.Activities.AsNoTracking()
.Where(a => a.ProjectId == decodeResult.Value)
.OrderByDescending(a => a.CreatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(activities?.Select(a => a.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(activities.Select(a => a.Map()).ToArray());
}));
public async ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Projects.AnyAsync(p => p.Name == request.Name && p.Sector == request.Sector && p.ProjectType == request.ProjectType, cancellationToken))
return Result.Fail("A similarly named project exists of the same project type and sector");
return Result.Fail(ValidationError.Create("A similarly named project exists of the same project type and sector"));
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
return Result.Fail($"Programme does not exist.");
return Result.Fail(NotFoundError.New);
context.Projects.Add(new Domain.Entities.Project
{
@@ -133,28 +109,20 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
CreatedAt = DateTime.UtcNow
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to create project");
}
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> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Result.Fail($"Project does not exist.");
return Result.Fail(NotFoundError.New);
if (request.MilestoneId.HasValue)
if (!await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken))
return Result.Fail($"Milestone does not exist.");
return Result.Fail(NotFoundError.New);
context.Activities.Add(new Domain.Entities.Activity
{
@@ -170,93 +138,69 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
CreatedAt = DateTime.UtcNow
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to create activity.");
}
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<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Milestone[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await 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));
}
}
return Result.Ok(milestones.Select(i => i.Map()).ToArray());
}));
public async Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken)
{
try
public async ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Indicator[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var evidences = await context.Indicators.AsNoTracking()
.Where(e => e.ProjectId == decodeResult.Value)
.OrderByDescending(o => o.Id)
.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<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken)
{
try
public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Milestone>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Milestone>(IdDecodeError.Create().CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var milestone = await context.Milestones.AsNoTracking().FirstOrDefaultAsync(m => m.Id == decodeResult.Value, 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");
: Result.Fail<Milestone>(NotFoundError.Create());
}));
}
catch (Exception ex)
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var milestones = await context.Milestones.AsNoTracking()
.OrderByDescending(m => m.CreatedAt)
@@ -264,19 +208,13 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.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));
}
}
return Result.Ok(milestones.Select(i => i.Map()).ToArray());
}));
public async Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public async ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Milestones.Where(m => m.Id == request.MilestoneId)
.ExecuteUpdateAsync(f => f
@@ -287,21 +225,13 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.SetProperty(u => u.UpdatedBy, userId)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
return changedTotal > 0
? Result.Ok()
: Result.Fail("Failed to update milestone");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.FailIf(changedTotal <= 0, UpdateError.New);
}));
public async Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken)
{
try
public async ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Projects.Where(p => p.Id == request.ProjectId)
.ExecuteUpdateAsync(f => f
@@ -314,36 +244,44 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.SetProperty(u => u.Sector, request.Sector)
.SetProperty(u => u.ProjectType, request.ProjectType), cancellationToken);
return changedTotal > 0
? Result.Ok()
: Result.Fail("Failed to update project");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.FailIf(changedTotal <= 0, UpdateError.New);
}));
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken)
{
try
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(activityIdHash);
if (decodeResult.IsFailed)
return Result.Fail(decodeResult.Errors);
return Result.Fail<Domain.Models.Activity>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var activity = await context.Activities.AsNoTracking().FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken);
var activity = await context.Activities.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken);
return activity is not null
? Result.Ok(activity.Map())
: Result.Fail("Failed to get specific activity.");
}
catch (Exception ex)
: Result.Fail<Domain.Models.Activity>(NotFoundError.New);
}));
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Risk[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var risks = await context.Risks.AsNoTracking()
.Where(r => r.ProjectId == decodeResult.Value)
.OrderByDescending(r => r.CreatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(risks.Select(i => i.Map()).ToArray());
}));
}
@@ -1,78 +1,50 @@
using PostFundManagement.Domain.Errors;
namespace PostFundManagement.Application.Services.Shared;
public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
{
public async ValueTask<Result> DeleteAsync(string key)
{
try
public async ValueTask<Result> DeleteAsync(string key) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await storage.DeleteAsync(key);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
}));
public async ValueTask<Result> SaveAsync(string key, string value)
{
try
public async ValueTask<Result> SaveAsync(string key, string value) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await storage.SetAsync(key, value);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
}));
public async ValueTask<Result> SaveAsync<TValue>(string key, TValue value) where TValue : class
{
try
public async ValueTask<Result> SaveAsync<TValue>(string key, TValue value) where TValue : class =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await storage.SetAsync(key, value);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
}));
public async ValueTask<Result<string>> GetAsync(string key)
{
try
public async ValueTask<Result<string>> GetAsync(string key) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
var retrieval = await storage.GetAsync<string>(key);
return retrieval.Success && !string.IsNullOrWhiteSpace(retrieval.Value)
? Result.Ok(retrieval.Value)
: Result.Fail($"Could not find object by key {key}");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail(NotFoundError.Create($"Could not find object by key {key}"));
}));
public async ValueTask<Result<TValue>> GetAsync<TValue>(string key) where TValue : class
{
try
public async ValueTask<Result<TValue>> GetAsync<TValue>(string key) where TValue : class =>
await Result.Try((Func<ValueTask<Result<TValue>>>)(async () =>
{
var retrieval = await storage.GetAsync<TValue>(key);
return retrieval.Success && retrieval.Value is not null
? Result.Ok(retrieval.Value)
: Result.Fail($"Could not find object by key {key}");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail(NotFoundError.Create($"Could not find object by key {key}"));
}));
}
@@ -7,6 +7,6 @@ namespace PostFundManagement.Application.Services.Shared;
public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractBucketName)] IAmazonS3 amazonS3) :
S3ServiceBase(amazonS3), IS3Service
{
protected override string BucketName => configuration.GetSection($"{ContractS3SettingsSection}:BucketName").Value ?? "";
protected override string CdnBaseUrl => configuration.GetSection($"{ContractS3SettingsSection}:CdnBaseUrl").Value ?? "";
protected override string BucketName => configuration.GetSection($"{ContractS3SettingsSection}:BucketName").Value ?? string.Empty;
protected override string CdnBaseUrl => configuration.GetSection($"{ContractS3SettingsSection}:CdnBaseUrl").Value ?? string.Empty;
}
@@ -7,6 +7,6 @@ namespace PostFundManagement.Application.Services.Shared;
public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceBucketName)] IAmazonS3 amazonS3) :
S3ServiceBase(amazonS3), IS3Service
{
protected override string BucketName => configuration.GetSection($"{EvidenceS3SettingsSection}:BucketName").Value ?? "";
protected override string CdnBaseUrl => configuration.GetSection($"{EvidenceS3SettingsSection}:CdnBaseUrl").Value ?? "";
protected override string BucketName => configuration.GetSection($"{EvidenceS3SettingsSection}:BucketName").Value ?? string.Empty;
protected override string CdnBaseUrl => configuration.GetSection($"{EvidenceS3SettingsSection}:CdnBaseUrl").Value ?? string.Empty;
}
@@ -7,6 +7,6 @@ namespace PostFundManagement.Application.Services.Shared;
public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralBucketName)] IAmazonS3 amazonS3) :
S3ServiceBase(amazonS3), IS3Service
{
protected override string BucketName => configuration.GetSection($"{GeneralS3SettingsSection}:BucketName").Value ?? "";
protected override string CdnBaseUrl => configuration.GetSection($"{GeneralS3SettingsSection}:CdnBaseUrl").Value ?? "";
protected override string BucketName => configuration.GetSection($"{GeneralS3SettingsSection}:BucketName").Value ?? string.Empty;
protected override string CdnBaseUrl => configuration.GetSection($"{GeneralS3SettingsSection}:CdnBaseUrl").Value ?? string.Empty;
}
@@ -1,6 +1,7 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
namespace PostFundManagement.Application.Shared;
namespace PostFundManagement.Application.Services.Shared;
public sealed partial class HashService(IHashids hasher) : IService
{
@@ -31,59 +32,55 @@ public sealed partial class HashService(IHashids hasher) : IService
public static Result<string> ToMd5Hash(string input)
{
if (string.IsNullOrEmpty(input))
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
return Result.Fail<string>(ValidationError.Create("Input content cannot be null or empty for MD5 processing."));
byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
}
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input)
? Result.Fail<string>("Input must be a valid hexadecimal string.")
? Result.Fail<string>(ValidationError.Create("Input must be a valid hexadecimal string."))
: Result.Ok(hasher.EncodeHex(input));
public Result<string> HashEncodeIntId(int id) => id < 0
? Result.Fail<string>("Id cannot be negative.")
? Result.Fail<string>(ValidationError.Create("Id cannot be negative."))
: Result.Ok(hasher.Encode(id));
public Result<string> HashEncodeLongId(long id) => id < 0
? Result.Fail<string>("Id cannot be negative.")
? Result.Fail<string>(ValidationError.Create("Id cannot be negative."))
: Result.Ok(hasher.EncodeLong(id));
public Result<int> DecodeIntIdHash(string hash)
{
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<int>("Invalid token layout.");
if (string.IsNullOrWhiteSpace(hash))
return Result.Fail<int>(ValidationError.Create("Invalid token layout."));
int[] decoded = hasher.Decode(hash);
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<int>("Invalid or modified Int hash token.");
return decoded.Length == 1
? Result.Ok(decoded[0]) :
Result.Fail<int>(ValidationError.Create("Invalid or modified Int hash token."));
}
public Result<long> DecodeLongIdHash(string hash)
{
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<long>("Invalid token layout.");
if (string.IsNullOrWhiteSpace(hash))
return Result.Fail<long>(ValidationError.Create("Invalid token layout."));
long[] decoded = hasher.DecodeLong(hash);
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<long>("Invalid or modified Long hash token.");
return decoded.Length == 1 ? Result.Ok(decoded[0]) :
Result.Fail<long>(ValidationError.Create("Invalid or modified Long hash token."));
}
public Result<string> DecodeHexHash(string hex)
{
try
public async Task<Result<string>> DecodeHexHash(string hex) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
string decoded = hasher.DecodeHex(hex);
return string.IsNullOrEmpty(decoded)
? Result.Fail<string>("Invalid or corrupted hex hash.")
? Result.Fail<string>(ValidationError.Create("Invalid or corrupted hex hash."))
: Result.Ok(decoded);
}
catch (FormatException fex)
{
return Result.Fail<string>(new Error("Invalid hash structure.").CausedBy(fex));
}
catch (Exception ex)
{
return Result.Fail<string>(new Error(ex.Message).CausedBy(ex));
}
}
}));
}
@@ -1,24 +1,20 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Shared;
public sealed class IdentityService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
{
public async ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
public async ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
return !await context.Users.AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken)
? Result.Fail("Invalid user")
: Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
var userExistsAndActive = await context.Users
.AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken);
return Result.FailIf(!userExistsAndActive, NotFoundError.Create());
}));
}
@@ -1,3 +1,4 @@
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Security.Configuration;
using PostFundManagement.Domain.Security.Models;
using PostFundManagement.Domain.Security.Sdk;
@@ -8,24 +9,21 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
{
private readonly SecurityClientSettings clientSettings = clientOptions.Value;
public async Task<Result<Domain.Security.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<Domain.Security.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
{
var request = new Domain.Security.Models.TokenRequest
using var response = await connectApi.GetToken(new Domain.Security.Models.TokenRequest
{
ClientId = clientSettings.ClientId,
ClientSecret = clientSettings.ClientSecret,
GrantType = clientSettings.GrantType,
Scope = clientSettings.Scope,
};
using var response = await connectApi.GetToken(request, cancellationToken);
}, cancellationToken);
var contentRaw = await response.Content.ReadAsStringAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(contentRaw))
return Result.Fail(new Error($"The authentication endpoint returned an empty payload. Status code: {response.StatusCode}"));
return Result.Fail(ValidationError.Create($"The authentication endpoint returned an empty payload. Status code: {response.StatusCode}"));
if (response.IsSuccessStatusCode)
{
@@ -33,34 +31,15 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken)
? Result.Ok(tokenResponse)
: Result.Fail<Domain.Security.Models.TokenResponse>(new Error("Authentication succeeded, but no access token was found in the response payload."));
: Result.Fail<Domain.Security.Models.TokenResponse>(NotFoundError.Create("Authentication succeeded, but no access token was found in the response payload."));
}
try
return await Result.Try((Func<ValueTask<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
{
var errorResult = JsonSerializer.Deserialize<TokenErrorResponse>(contentRaw);
if (errorResult != null)
{
string summary = $"{errorResult.Error}: {errorResult.ErrorDescription}";
return Result.Fail(new Error(summary));
}
}
catch
{
return Result.Fail(new Error($"Authentication failed: {contentRaw}"));
}
return Result.Fail(new Error($"Authentication failed with status code: {response.StatusCode}"));
}
catch (OperationCanceledException ex)
{
return Result.Fail(new Error("The token generation request was canceled.").CausedBy(ex));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.FailIf(errorResult != null,
CreateError.Create($"{errorResult!.Error}: {errorResult.ErrorDescription}"));
}));
}));
}
@@ -1,5 +1,6 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database;
@@ -8,11 +9,10 @@ namespace PostFundManagement.Application.Services.Users;
public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
{
public async ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> SyncUserAsync(Guid userId, string email, 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);
var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
@@ -32,21 +32,13 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
context.Users.Update(user);
}
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to sync user");
}
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<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<User[]>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Users.AsNoTracking();
@@ -67,54 +59,34 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
else
query = query.OrderBy(u => u.Id);
var totalCount = await query.CountAsync(cancellationToken);
var users = await query.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize).ToListAsync(cancellationToken);
return Result.Ok(users?.Select(u => u.Map()).ToArray() ?? []);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.Ok(users.Select(u => u.Map()).ToArray());
}));
public async ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default)
{
try
public async ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, 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);
var changedTotal = await context.Users
.Where(f => f.Id == userId)
.ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken);
return changedTotal > 0
? Result.Ok()
: Result.Fail("Failed to update user activity status.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
return Result.FailIf(changedTotal <= 0, UpdateError.New);
}));
public async Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken)
{
try
public async ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<User>>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var user = await context.Users.AsNoTracking().FirstOrDefaultAsync(f => f.Id == userId, cancellationToken);
var user = await context.Users.AsNoTracking()
.FirstOrDefaultAsync(f => f.Id == userId, cancellationToken);
return user is not null
? Result.Ok(user.Map())
: Result.Fail("Failed to find specific user.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
: Result.Fail(NotFoundError.New);
}));
}