Refactored endpoints to be more functional and more expressive
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using PostFundManagement.Application.Shared;
|
||||
using PostFundManagement.Application.Services.Shared;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
|
||||
namespace PostFundManagement.Application.Abstractions;
|
||||
|
||||
@@ -9,17 +10,16 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||
protected abstract string BucketName { get; }
|
||||
protected abstract string CdnBaseUrl { get; }
|
||||
|
||||
public virtual async Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
public virtual async ValueTask<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BucketName))
|
||||
return Result.Fail<string>("Bucket name is not configured.");
|
||||
return Result.Fail<string>(Domain.Errors.ValidationError.Create("Bucket name is not configured."));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CdnBaseUrl))
|
||||
return Result.Fail<string>("CDN base URL is not configured.");
|
||||
return Result.Fail<string>(Domain.Errors.ValidationError.Create("CDN base URL is not configured."));
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
await using var stream = new MemoryStream();
|
||||
|
||||
await fileStream.CopyToAsync(stream, cancellationToken);
|
||||
await fileStream.DisposeAsync();
|
||||
@@ -28,8 +28,8 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||
|
||||
var fileHash = HashService.StreamToSha256Hash(stream);
|
||||
|
||||
if(string.IsNullOrWhiteSpace(fileHash))
|
||||
return Result.Fail<string>("Failed to compute file hash.");
|
||||
if (string.IsNullOrWhiteSpace(fileHash))
|
||||
return Result.Fail<string>(CreateError.Create("Failed to compute file hash."));
|
||||
|
||||
var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}";
|
||||
|
||||
@@ -46,38 +46,24 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||
|
||||
var response = await Client.PutObjectAsync(putRequest, cancellationToken);
|
||||
|
||||
return response.HttpStatusCode != System.Net.HttpStatusCode.OK
|
||||
? Result.Fail<string>($"Failed to upload {fileName} to S3.")
|
||||
return response.HttpStatusCode != HttpStatusCode.OK
|
||||
? Result.Fail<string>(CreateError.Create($"Failed to upload {fileName} to S3."))
|
||||
: Result.Ok($"{CdnBaseUrl}/{fileKey}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
public virtual async Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
public virtual async ValueTask<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<ValueTask<Result>>)(async () =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BucketName))
|
||||
return Result.Fail("Bucket name is not configured.");
|
||||
return Result.Fail(NotFoundError.Create("Bucket name is not configured."));
|
||||
|
||||
var deleteRequest = new DeleteObjectRequest
|
||||
var response = await Client.DeleteObjectAsync(new DeleteObjectRequest
|
||||
{
|
||||
BucketName = BucketName,
|
||||
Key = fileKey
|
||||
};
|
||||
}, cancellationToken);
|
||||
|
||||
var response = await Client.DeleteObjectAsync(deleteRequest, cancellationToken);
|
||||
|
||||
return response.HttpStatusCode != System.Net.HttpStatusCode.NoContent
|
||||
? Result.Fail($"Failed to delete {fileKey} from S3.")
|
||||
: Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error($"Error deleting {fileKey} from S3: {ex.Message}").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
return Result.FailIf(response.HttpStatusCode == HttpStatusCode.NoContent,
|
||||
UpdateError.Create($"Failed to delete {fileKey} from S3."));
|
||||
}));
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using Asp.Versioning;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Security;
|
||||
using PostFundManagement.Domain.Security.Configuration;
|
||||
|
||||
@@ -19,7 +19,7 @@ public static class General
|
||||
|
||||
public static Guid GetUserId(this HttpContext context) => (Guid)context.Items[Domain.Extensions.Constants.UserIdItemKey]!;
|
||||
|
||||
public static string GetUserEmail(this HttpContext context) => (string)context.Items[Domain.Extensions.Constants.EmaiItemKey]!;
|
||||
public static string GetUserEmail(this HttpContext context) => (string)context.Items[Domain.Extensions.Constants.EmailtemKey]!;
|
||||
|
||||
public static (bool Decoded, Guid userId) GetUserId(this ClaimsPrincipal principal)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using PostFundManagement.Application.Shared;
|
||||
using PostFundManagement.Application.Services.Shared;
|
||||
using PostFundManagement.Domain.Configuration.Hash;
|
||||
|
||||
namespace PostFundManagement.Application.Extensions;
|
||||
|
||||
@@ -26,9 +26,8 @@ public sealed class ActiveUserFilter(IdentityService identityService) : IEndpoin
|
||||
return (object?)Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
|
||||
|
||||
context.HttpContext.Items[UserIdItemKey] = userId;
|
||||
context.HttpContext.Items[EmaiItemKey] = email;
|
||||
context.HttpContext.Items[EmailtemKey] = email;
|
||||
|
||||
return await next(context);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user