Refactored endpoints to be more functional and more expressive

This commit is contained in:
2026-09-01 11:23:25 +02:00
parent 346cb8da5f
commit 2df5b5c2ed
86 changed files with 1565 additions and 1575 deletions
+3
View File
@@ -43,6 +43,9 @@ dotnet_diagnostic.MA0004.severity = none
[**/*{Enums,Records}.cs] [**/*{Enums,Records}.cs]
dotnet_diagnostic.MA0048.severity = none dotnet_diagnostic.MA0048.severity = none
[*.{cs,vb}]
dotnet_diagnostic.IDE0039.severity = none
#### Core EditorConfig Options #### #### Core EditorConfig Options ####
# Indentation and spacing # Indentation and spacing
@@ -1,9 +1,11 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Communications; using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Communications; namespace PostFundManagement.Api.Endpoints.Communications;
@@ -14,20 +16,20 @@ public sealed class GetApplicantInviteEndpoint : IEndpoint
{ {
builder.MapGet("api/communications/invites/applicant/{userId:Guid}", async (Guid userId, CommunicationsService service, builder.MapGet("api/communications/invites/applicant/{userId:Guid}", async (Guid userId, CommunicationsService service,
HttpContext context, CancellationToken cancellationToken = default) => HttpContext context, CancellationToken cancellationToken = default) =>
{ await service.GetApplicantInviteAsync(userId, cancellationToken) switch
var result = await service.GetApplicantInviteAsync(userId, cancellationToken); {
{ IsSuccess: true, Value: var invite } => Results.Ok(invite),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok(result.Value) var r => Results.InternalServerError(r.Errors.ToProblems()),
: Results.NotFound(); })
}) .WithDescription("Gets an applicant's invite")
.WithDescription("Gets an applicant's invite") .WithName(typeof(GetApplicantInviteEndpoint).ToEndpointName())
.WithName(typeof(GetApplicantInviteEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces<Invite>(StatusCodes.Status200OK)
.Produces<Invite>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status401Unauthorized) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Communications) .WithTags(EndpointTags.Communications)
.CacheOutput(); .CacheOutput();
} }
} }
@@ -6,30 +6,29 @@ using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using static PostFundManagement.Application.Extensions.General; using static PostFundManagement.Application.Extensions.General;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Communications; namespace PostFundManagement.Api.Endpoints.Communications;
[ApiVersionTarget(1)] [ApiVersionTarget(1)]
public class GetInvitesEndpoint : IEndpoint public sealed class GetInvitesEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapGet("api/communications/invites/{page:int}/{pageSize:int}", async (CommunicationsService service, builder.MapGet("api/communications/invites/{page:int}/{pageSize:int}", async (CommunicationsService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetInvitesAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetInvitesAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var invites } => Results.Ok(invites),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get a list of sent invitations")
}) .WithName(typeof(GetInvitesEndpoint).ToEndpointName())
.WithDescription("Get a list of sent invitations") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetInvitesEndpoint).ToEndpointName()) .Produces<Invite[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Invite[]>(StatusCodes.Status200OK) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Communications)
.Produces(StatusCodes.Status401Unauthorized) .CacheOutput();
.WithTags(EndpointTags.Communications)
.CacheOutput();
} }
} }
@@ -1,31 +1,34 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Communications; using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Communications; namespace PostFundManagement.Api.Endpoints.Communications;
[ApiVersionTarget(1)] [ApiVersionTarget(1)]
public class SendInviteEndpoint : IEndpoint public sealed class SendInviteEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapPost("api/communications/invite", async (InviteRequest request, builder.MapPost("api/communications/invite", async (InviteRequest request,
HttpContext context, CommunicationsService service, CancellationToken cancellationToken = default) => HttpContext context, CommunicationsService service, CancellationToken cancellationToken = default) =>
{ await service.SendInviteAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.SendInviteAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true, Value: var invite } => Results.Ok(invite),
return result.IsSuccess var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
? Results.Ok(result.Value) var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.BadRequest(result.Errors.ToProblems()); })
}) .WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)") .WithName(typeof(SendInviteEndpoint).ToEndpointName())
.WithName(typeof(SendInviteEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces<Invite>(StatusCodes.Status200OK)
.Produces<Domain.Entities.Invite>(StatusCodes.Status200OK) .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Communications); .WithTags(EndpointTags.Communications);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -13,19 +15,21 @@ public class CreateIndicatorEndpoint : IEndpoint
{ {
builder.MapPost("api/evidence/indicators", async (CreateIndicatorRequest request, HttpContext context, builder.MapPost("api/evidence/indicators", async (CreateIndicatorRequest request, HttpContext context,
EvidenceService service, CancellationToken cancellationToken = default) => EvidenceService service, CancellationToken cancellationToken = default) =>
{ await service.CreateIndicatorAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateIndicatorAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok() var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Define indicators and baselines/targets (M08)") .WithDescription("Define indicators and baselines/targets (M08)")
.WithName(typeof(CreateIndicatorEndpoint).ToEndpointName()) .WithName(typeof(CreateIndicatorEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence); .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Evidence);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -13,19 +15,20 @@ public class CreateMilestoneEndpoint : IEndpoint
{ {
builder.MapPost("api/evidence/milestones", async (CreateMilestoneRequest request, HttpContext context, builder.MapPost("api/evidence/milestones", async (CreateMilestoneRequest request, HttpContext context,
EvidenceService service, CancellationToken cancellationToken = default) => EvidenceService service, CancellationToken cancellationToken = default) =>
{ await service.CreateMilestoneAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateMilestoneAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok() var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Define contractual milestones (M07)") .WithDescription("Define contractual milestones (M07)")
.WithName(typeof(CreateMilestoneRequest).ToEndpointName()) .WithName(typeof(CreateMilestoneRequest).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status401Unauthorized) .Produces<Problems>(StatusCodes.Status400BadRequest)
.WithTags(EndpointTags.Evidence); .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Evidence);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -11,23 +13,24 @@ public sealed class GetEvidenceEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapGet("api/evidence/{evidenceId}", async (string evidenceId, EvidenceService service, builder.MapGet("api/evidence/{evidenceId}", async (string evidenceId, EvidenceService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetEvidenceAsync(evidenceId, cancellationToken) switch
var result = await service.GetEvidenceAsync(evidenceId, cancellationToken); {
{ IsSuccess: true, Value: var evidence } => Results.Ok(evidence),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.NotFound(); var r => Results.InternalServerError(r.Errors.ToProblems())
})
}) .WithDescription("Get a specific evidence record and assiciated file URLs")
.WithDescription("Get a specific evidence record and assiciated file URLs") .WithName(typeof(GetEvidenceEndpoint).ToEndpointName())
.WithName(typeof(GetEvidenceEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces<Domain.Models.Evidence>(StatusCodes.Status200OK)
.Produces<Domain.Models.Evidence>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden)
.WithTags(EndpointTags.Evidence) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.CacheOutput(); .WithTags(EndpointTags.Evidence)
.CacheOutput();
} }
} }
@@ -4,6 +4,7 @@ using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -14,20 +15,18 @@ public class GetEvidenceFilesEndpoint : IEndpoint
{ {
builder.MapGet("api/evidence/files/{page:int}/{pageSize:int}", async (EvidenceService service, builder.MapGet("api/evidence/files/{page:int}/{pageSize:int}", async (EvidenceService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetEvidenceFilesAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetEvidenceFilesAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var evidences} => Results.Ok(evidences),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get a list of all uploaded evidence files metadata")
}) .WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName())
.WithDescription("Get a list of all uploaded evidence files metadata") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName()) .Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Evidence)
.Produces(StatusCodes.Status401Unauthorized) .CacheOutput();
.WithTags(EndpointTags.Evidence)
.CacheOutput();
} }
} }
@@ -1,9 +1,11 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -14,19 +16,21 @@ public sealed class GetIndicatorEndpoint : IEndpoint
{ {
builder.MapGet("api/evidence/indicators/{indicatorId}", async (string indicatorId, EvidenceService service, builder.MapGet("api/evidence/indicators/{indicatorId}", async (string indicatorId, EvidenceService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetIndicatorAsync(indicatorId, cancellationToken) switch
var result = await service.GetIndicatorAsync(indicatorId, cancellationToken); {
{ IsSuccess: true, Value: var indicator } => Results.Ok(indicator),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.NotFound(); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get a specific indicator.") .WithDescription("Get a specific indicator.")
.WithName(typeof(GetIndicatorEndpoint).ToEndpointName()) .WithName(typeof(GetIndicatorEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Indicator>(StatusCodes.Status200OK) .Produces<Indicator>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Evidence); .WithTags(EndpointTags.Evidence);
} }
} }
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -15,19 +16,17 @@ public class GetIndicatorsEndpoint : IEndpoint
{ {
builder.MapGet("api/evidence/indicators/{page:int}/{pageSize:int}", async (EvidenceService service, builder.MapGet("api/evidence/indicators/{page:int}/{pageSize:int}", async (EvidenceService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetIndicatorsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetIndicatorsAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var indicators } => Results.Ok(indicators),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems()),
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get a list of indicators")
}) .WithName(typeof(GetIndicatorsEndpoint).ToEndpointName())
.WithDescription("Get a list of indicators") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetIndicatorsEndpoint).ToEndpointName()) .Produces<Indicator[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Indicator[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Evidence);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -13,20 +15,21 @@ public class GetMilestoneEvidenceEndpoint : IEndpoint
{ {
builder.MapGet("api/evidence/milestone/{milestoneId}", async (string milestoneId, EvidenceService service, builder.MapGet("api/evidence/milestone/{milestoneId}", async (string milestoneId, EvidenceService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetMilestoneEvidencesAsync(milestoneId, cancellationToken) switch
var result = await service.GetMilestoneEvidencesAsync(milestoneId, cancellationToken); {
{ IsSuccess: true, Value: var evidences } => Results.Ok(evidences),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.NotFound(); })
}) .WithDescription("Get a list of all uploaded evidence metadata for a specific mliestone")
.WithDescription("Get a list of all uploaded evidence metadata for a specific mliestone") .WithName(typeof(GetMilestoneEvidenceEndpoint).ToEndpointName())
.WithName(typeof(GetMilestoneEvidenceEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.CacheOutput(); .WithTags(EndpointTags.Evidence)
.CacheOutput();
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -13,20 +15,21 @@ public class GetProjectEvidenceEndpoint : IEndpoint
{ {
builder.MapGet("api/evidence/milestone/{projectId}", async (string projectId, EvidenceService service, builder.MapGet("api/evidence/milestone/{projectId}", async (string projectId, EvidenceService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetProjectEvidencesAsync(projectId, cancellationToken) switch
var result = await service.GetProjectEvidencesAsync(projectId, cancellationToken); {
{ IsSuccess: true, Value: var evidence } => Results.Ok(evidence),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r => Results.InternalServerError(r.Errors.ToProblems()),
: Results.NotFound(); })
}) .WithDescription("Get a list of all uploaded evidence metadata for a specific project")
.WithDescription("Get a list of all uploaded evidence metadata for a specific project") .WithName(typeof(GetProjectEvidenceEndpoint).ToEndpointName())
.WithName(typeof(GetProjectEvidenceEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden)
.WithTags(EndpointTags.Evidence) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.CacheOutput(); .WithTags(EndpointTags.Evidence)
.CacheOutput();
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -13,19 +15,21 @@ public class RecordIndicatorAmountEndpoint : IEndpoint
{ {
builder.MapPost("api/evidence/indicators/{indicatorId}/actuals/{actualAmount:decimal}", async (string indicatorId, decimal actualAmount, builder.MapPost("api/evidence/indicators/{indicatorId}/actuals/{actualAmount:decimal}", async (string indicatorId, decimal actualAmount,
HttpContext context, EvidenceService service, CancellationToken cancellationToken = default) => HttpContext context, EvidenceService service, CancellationToken cancellationToken = default) =>
{ await service.RecordIndicatorActualAmountAsync(indicatorId, actualAmount, context.GetUserId(), cancellationToken) switch
var result = await service.RecordIndicatorActualAmountAsync(indicatorId, actualAmount, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok() var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Record actual values for indicators (M08)") .WithDescription("Record actual values for indicators (M08)")
.WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName()) .WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden)
.WithTags(EndpointTags.Evidence); .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Evidence);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Evidence; namespace PostFundManagement.Api.Endpoints.Evidence;
@@ -25,11 +27,12 @@ public class UploadEvidenceFilesEndpoint : IEndpoint
if (file == null || file.Length == 0) if (file == null || file.Length == 0)
return Results.BadRequest("No file uploaded or file is empty."); return Results.BadRequest("No file uploaded or file is empty.");
var result = await service.UploadEvidenceFileAsync(userId, form, file, cancellationToken); return await service.UploadEvidenceFileAsync(userId, form, file, cancellationToken) switch
{
return result.IsSuccess { IsSuccess: true } => Results.Ok(),
? Results.Ok() var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems())
};
}) })
.RequireAuthorization() .RequireAuthorization()
.DisableAntiforgery() .DisableAntiforgery()
@@ -37,7 +40,8 @@ public class UploadEvidenceFilesEndpoint : IEndpoint
.WithName(typeof(UploadEvidenceFilesEndpoint).ToEndpointName()) .WithName(typeof(UploadEvidenceFilesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence); .WithTags(EndpointTags.Evidence);
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution; using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution; namespace PostFundManagement.Api.Endpoints.Execution;
@@ -11,21 +13,23 @@ public class ApproveDisbursementEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapPost("api/execution/disbursements/{disbursementId}/approve", async (string disbursementId, builder.MapPost("api/execution/disbursements/{disbursementId}/approve", async (string disbursementId,
ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) => ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
{ await service.ApproveDisbursementAsync(disbursementId, context.GetUserId(), cancellationToken) switch
var result = await service.ApproveDisbursementAsync(disbursementId, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok() var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Approve disbursement and release payment (M06)") .WithDescription("Approve disbursement and release payment (M06)")
.WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName()) .WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden)
.WithTags(EndpointTags.Execution); .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution; using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution; namespace PostFundManagement.Api.Endpoints.Execution;
@@ -13,19 +15,21 @@ public class CreateDisbursementEndpoint : IEndpoint
{ {
builder.MapPost("api/execution/disbursements", async (CreateDisbursementRequest request, builder.MapPost("api/execution/disbursements", async (CreateDisbursementRequest request,
ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) => ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
{ await service.CreateDisbursementAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateDisbursementAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok() var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems()),
}) })
.WithDescription("Create a new disbursement schedule (M06)") .WithDescription("Create a new disbursement schedule (M06)")
.WithName(typeof(CreateDisbursementEndpoint).ToEndpointName()) .WithName(typeof(CreateDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution); .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution; using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution; namespace PostFundManagement.Api.Endpoints.Execution;
@@ -13,19 +15,21 @@ public class CreateRiskEndpoint : IEndpoint
{ {
builder.MapPost("api/execution/risks", async (CreateRiskRequest request, ExecutionService service, builder.MapPost("api/execution/risks", async (CreateRiskRequest request, ExecutionService service,
HttpContext context, CancellationToken cancellationToken = default) => HttpContext context, CancellationToken cancellationToken = default) =>
{ await service.CreateRiskAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateRiskAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok() var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Add risk to risk register (M09)") .WithDescription("Add risk to risk register (M09)")
.WithName(typeof(CreateRiskEndpoint).ToEndpointName()) .WithName(typeof(CreateRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution); .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
} }
} }
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public sealed class GetDisbursementEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/disbursements/{disbursementId}", async (string disbursementId, ExecutionService service,
CancellationToken cancellationToken = default) =>
await service.GetDisbursementAsync(disbursementIdHash: disbursementId, cancellationToken) switch
{
{ IsSuccess: true, Value: var disbursement } => Results.Ok(disbursement),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific disbursement")
.WithName(typeof(GetDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Disbursement>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
}
}
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution; namespace PostFundManagement.Api.Endpoints.Execution;
@@ -15,19 +16,18 @@ public class GetDisbursementsEndpoint : IEndpoint
{ {
builder.MapGet("api/execution/disbursements", async (ExecutionService service, builder.MapGet("api/execution/disbursements", async (ExecutionService service,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{ await service.GetDisbursementsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetDisbursementsAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var disbursements } => Results.Ok(disbursements),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.NotFound(); .WithDescription("Get a list of disbursements")
}) .WithName(typeof(GetDisbursementsEndpoint).ToEndpointName())
.WithDescription("Get a list of disbursements") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetDisbursementsEndpoint).ToEndpointName()) .Produces<Disbursement[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Disbursement[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status404NotFound) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Execution);
.WithTags(EndpointTags.Execution);
} }
} }
@@ -1,32 +0,0 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public class GetProjectRisksEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/risks/project/{projectId}", async (string projectId,
ExecutionService service, CancellationToken cancellationToken = default) =>
{
var result = await service.GetProjectRisksAsync(projectId, cancellationToken);
return result.IsSuccess
? Results.Ok(result.Value)
: Results.NotFound();
})
.WithDescription("Get a list of risks for a project")
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Risk[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution);
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public sealed class GetRiskEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/risks/{riskId}", async (string riskId, ExecutionService service,
CancellationToken cancellationToken = default) =>
await service.GetRiskAsync(riskIdHash: riskId, cancellationToken) switch
{
{ IsSuccess: true, Value: var risk } => Results.Ok(risk),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific risk")
.WithName(typeof(GetRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Risk>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
}
}
@@ -4,6 +4,7 @@ using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution; namespace PostFundManagement.Api.Endpoints.Execution;
@@ -14,18 +15,17 @@ public class GetRisksEndpoint : IEndpoint
{ {
builder.MapGet("api/execution/risks", async (ExecutionService service, builder.MapGet("api/execution/risks", async (ExecutionService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetRisksAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetRisksAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var risks } => Results.Ok(risks),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.NotFound(); .WithDescription("Get a list of all risks")
}) .WithName(typeof(GetRisksEndpoint).ToEndpointName())
.WithDescription("Get a list of all risks") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetRisksEndpoint).ToEndpointName()) .Produces(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status200OK) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Execution);
.WithTags(EndpointTags.Execution);
} }
} }
@@ -0,0 +1,33 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public sealed class UpdateDisbursementEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/execution/disbursements", async (UpdateDisbursementRequest request, HttpContext context,
ExecutionService service, CancellationToken cancellationToken = default) =>
await service.UpdateDisbursementAsync(request, userId: context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update the details of a specific disbursement")
.WithName(typeof(UpdateDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Execution);
}
}
@@ -0,0 +1,33 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public sealed class UpdateRiskEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/execution/risks", async (UpdateRiskRequest request,
ExecutionService service, CancellationToken cancellationToken = default) =>
await service.UpdateRiskAsync(request, cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update the details of a specific risk")
.WithName(typeof(UpdateRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
}
}
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance; using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance; namespace PostFundManagement.Api.Endpoints.Governance;
@@ -13,19 +15,19 @@ public class CreateOrganisationEndpoint : IEndpoint
{ {
builder.MapPost("api/governance/organisations", async (CreateOrganisationRequest request, HttpContext context, builder.MapPost("api/governance/organisations", async (CreateOrganisationRequest request, HttpContext context,
GovernanceService service, CancellationToken cancellationToken = default) => GovernanceService service, CancellationToken cancellationToken = default) =>
{ await service.CreateOrganisationAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateOrganisationAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
? Results.Ok() var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.BadRequest(result.Errors.ToProblems()); })
}) .WithDescription("Create a new organization profile")
.WithDescription("Create a new organization profile") .WithName(typeof(CreateOrganisationEndpoint).ToEndpointName())
.WithName(typeof(CreateOrganisationEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status200OK) .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance); .WithTags(EndpointTags.Governance);
} }
} }
@@ -3,6 +3,7 @@ using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance; namespace PostFundManagement.Api.Endpoints.Governance;
@@ -13,19 +14,17 @@ public class DashboardEndpoint : IEndpoint
{ {
builder.MapGet("api/governance/dashboard", async (GovernanceService service, builder.MapGet("api/governance/dashboard", async (GovernanceService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetDashboardAsync(cancellationToken) switch
var result = await service.GetDashboardAsync(cancellationToken); {
{ IsSuccess: true, Value: var dashboard } => Results.Ok(dashboard),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get executive portfolio dashboard metrics and KPIs")
}) .WithName(typeof(DashboardEndpoint).ToEndpointName())
.WithDescription("Get executive portfolio dashboard metrics and KPIs") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(DashboardEndpoint).ToEndpointName()) .Produces<Dashboard>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Dashboard>(StatusCodes.Status200OK) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Governance);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
} }
} }
@@ -4,6 +4,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance; namespace PostFundManagement.Api.Endpoints.Governance;
@@ -14,19 +15,17 @@ public class GetInstrumentsEndpoint : IEndpoint
{ {
builder.MapGet("api/governance/instruments", async (GovernanceService service, builder.MapGet("api/governance/instruments", async (GovernanceService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetInstrumentsAsync(cancellationToken) switch
var result = await service.GetInstrumentsAsync(cancellationToken); {
{ IsSuccess: true, Value: var instrumenrs } => Results.Ok(instrumenrs),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems()),
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get all funding instruments")
}) .WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
.WithDescription("Get all funding instruments") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName()) .Produces<Instrument[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Instrument[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Governance);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
} }
} }
@@ -1,9 +1,11 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance; using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance; namespace PostFundManagement.Api.Endpoints.Governance;
@@ -14,19 +16,21 @@ public class GetOrganisationByIdEndpoint : IEndpoint
{ {
builder.MapGet("api/governance/organisations/{organisationId}", async (string organisationId, builder.MapGet("api/governance/organisations/{organisationId}", async (string organisationId,
GovernanceService service, CancellationToken cancellationToken = default) => GovernanceService service, CancellationToken cancellationToken = default) =>
{ await service.GetOrganisationByIdAsync(organisationId, cancellationToken) switch
var result = await service.GetOrganisationByIdAsync(organisationId, cancellationToken); {
{ IsSuccess: true, Value: var organisation } => Results.Ok(organisationId),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.NotFound(); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get organization profile by ID") .WithDescription("Get organization profile by ID")
.WithName(typeof(GetOrganisationByIdEndpoint).ToEndpointName()) .WithName(typeof(GetOrganisationByIdEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Organisation>(StatusCodes.Status200OK) .Produces<Organisation>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance); .Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance);
} }
} }
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance; namespace PostFundManagement.Api.Endpoints.Governance;
@@ -15,19 +16,17 @@ public class GetOrganisationsEndpoint : IEndpoint
{ {
builder.MapGet("api/governance/organisations", async (GovernanceService service, builder.MapGet("api/governance/organisations", async (GovernanceService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetOrganisationsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetOrganisationsAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var orgnanisations } => Results.Ok(orgnanisations),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get a paginated directory of organizations")
}) .WithName(typeof(GetOrganisationsEndpoint).ToEndpointName())
.WithDescription("Get a paginated directory of organizations") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetOrganisationsEndpoint).ToEndpointName()) .Produces<Organisation[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Organisation[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Governance);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
} }
} }
@@ -4,6 +4,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance; namespace PostFundManagement.Api.Endpoints.Governance;
@@ -14,19 +15,17 @@ public class GetPortfoliosEndpoint : IEndpoint
{ {
builder.MapGet("api/governance/portfolios", async (GovernanceService service, builder.MapGet("api/governance/portfolios", async (GovernanceService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetPorfoliosAsync(cancellationToken) switch
var result = await service.GetPorfoliosAsync(cancellationToken); {
{ IsSuccess: true, Value: var portfolios } => Results.Ok(portfolios),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get all portfolios")
}) .WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
.WithDescription("Get all portfolios") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName()) .Produces<Portfolio[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Portfolio[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Governance);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
} }
} }
@@ -4,6 +4,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance; namespace PostFundManagement.Api.Endpoints.Governance;
@@ -14,19 +15,17 @@ public class GetProgrammesEndpoint : IEndpoint
{ {
builder.MapGet("api/governance/programmes", async (GovernanceService service, builder.MapGet("api/governance/programmes", async (GovernanceService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetProgrammesAsync(cancellationToken) switch
var result = await service.GetProgrammesAsync(cancellationToken); {
{ IsSuccess: true, Value: var programmes } => Results.Ok(programmes),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get all programmes")
}) .WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
.WithDescription("Get all programmes") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName()) .Produces<Programme[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Programme[]>(StatusCodes.Status200OK) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Governance);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
@@ -13,19 +15,22 @@ public class CreateAwardEndpoint : IEndpoint
{ {
builder.MapPost("api/grants/awards", async (CreateAwardRequest request, HttpContext context, builder.MapPost("api/grants/awards", async (CreateAwardRequest request, HttpContext context,
GrantsService service, CancellationToken cancellationToken = default) => GrantsService service, CancellationToken cancellationToken = default) =>
{ await service.CreateAwardAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateAwardAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok() var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
}) var r => Results.InternalServerError(r.Errors.ToProblems())
.WithDescription("Register an approved funding award (M01)") })
.WithName(typeof(CreateAwardEndpoint).ToEndpointName()) .WithDescription("Register an approved funding award (M01)")
.MapToApiVersion(new ApiVersion(1)) .WithName(typeof(CreateAwardEndpoint).ToEndpointName())
.Produces(StatusCodes.Status200OK) .MapToApiVersion(new ApiVersion(1))
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants); .Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
@@ -13,18 +15,22 @@ public class CreateBudgetEndpoint : IEndpoint
{ {
builder.MapPost("api/grants/budgets", async (CreateBudgetRequest request, HttpContext context, builder.MapPost("api/grants/budgets", async (CreateBudgetRequest request, HttpContext context,
GrantsService service, CancellationToken cancellationToken = default) => GrantsService service, CancellationToken cancellationToken = default) =>
{ await service.CreateBudgetAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateBudgetAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
? Results.Ok() var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.BadRequest(result.Errors.ToProblems()); var r when r.HasError<CreateError>() => Results.BadRequest(),
}) var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create a new budget line (M05)") .WithDescription("Create a new budget line (M05)")
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName()) .WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants); .WithTags(EndpointTags.Grants);
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
@@ -13,19 +15,20 @@ public class CreateContractEndpoint : IEndpoint
{ {
builder.MapPost("api/grants/contracts", async (CreateContractRequest request, HttpContext context, builder.MapPost("api/grants/contracts", async (CreateContractRequest request, HttpContext context,
GrantsService service, CancellationToken cancellationToken = default) => GrantsService service, CancellationToken cancellationToken = default) =>
{ await service.CreateContractAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateContractAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok() var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Create a contract record from an approved award (M02)") .WithDescription("Create a contract record from an approved award (M02)")
.WithName(typeof(CreateContractEndpoint).ToEndpointName()) .WithName(typeof(CreateContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants); .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
@@ -13,19 +15,19 @@ public class ExecuteContractEndpoint : IEndpoint
{ {
builder.MapPost("api/grants/contracts/execute", async (ExecuteContractRequest request, HttpContext context, builder.MapPost("api/grants/contracts/execute", async (ExecuteContractRequest request, HttpContext context,
GrantsService service, CancellationToken cancellationToken = default) => GrantsService service, CancellationToken cancellationToken = default) =>
{ await service.ExecuteContractAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.ExecuteContractAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
? Results.Ok() var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.BadRequest(result.Errors.ToProblems()); })
}) .WithDescription("Execute the contract and lock the contractual version")
.WithDescription("Execute the contract and lock the contractual version") .WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status200OK) .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants); .WithTags(EndpointTags.Grants);
} }
} }
@@ -1,32 +1,36 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)] [ApiVersionTarget(1)]
public class GetAwardByIdEndpoint : IEndpoint public class GetAwardEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapGet("api/grants/awards/{awardId}", async (string awardId, GrantsService service, builder.MapGet("api/grants/awards/{awardId}", async (string awardId, GrantsService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetAwardAsync(awardId, cancellationToken) switch
var result = await service.GetAwardByIdAsync(awardId, cancellationToken); {
{ IsSuccess: true, Value: var award } => Results.Ok(award),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.NotFound(); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get award by ID") .WithDescription("Get award by ID")
.WithName(typeof(GetAwardByIdEndpoint).ToEndpointName()) .WithName(typeof(GetAwardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Award>(StatusCodes.Status200OK) .Produces<Award>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants); .WithTags(EndpointTags.Grants);
} }
} }
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
@@ -15,18 +16,16 @@ public class GetAwardsEndpoint : IEndpoint
{ {
builder.MapGet("api/grants/awards", async (GrantsService service, builder.MapGet("api/grants/awards", async (GrantsService service,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
{ await service.GetAwardsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetAwardsAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var awards } => Results.Ok(awards),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems());
})
.WithDescription("Get a list of awards") .WithDescription("Get a list of awards")
.WithName(typeof(GetAwardsEndpoint).ToEndpointName()) .WithName(typeof(GetAwardsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Award[]>(StatusCodes.Status200OK) .Produces<Award[]>(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants); .WithTags(EndpointTags.Grants);
} }
@@ -1,32 +1,36 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)] [ApiVersionTarget(1)]
public class GetBudgetByIdEndpoint : IEndpoint public class GetBudgetEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapGet("api/grants/budgets/{budgetId}", async (string budgetId, GrantsService service, builder.MapGet("api/grants/budgets/{budgetId}", async (string budgetId, GrantsService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetBudgetAsync(budgetId, cancellationToken) switch
var result = await service.GetBudgetByIdAsync(budgetId, cancellationToken); {
{ IsSuccess: true, Value: var budget } => Results.Ok(budget),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.NotFound(); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get budget by ID") .WithDescription("Get budget by ID")
.WithName(typeof(GetBudgetByIdEndpoint).ToEndpointName()) .WithName(typeof(GetBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Budget>(StatusCodes.Status200OK) .Produces<Budget>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants); .WithTags(EndpointTags.Grants);
} }
} }
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
@@ -15,18 +16,16 @@ public class GetBudgetsEndpoint : IEndpoint
{ {
builder.MapGet("api/grants/budgets", async (GrantsService service, builder.MapGet("api/grants/budgets", async (GrantsService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetBudgetsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetBudgetsAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var budgets } => Results.Ok(budgets),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems());
})
.WithDescription("Get all budgets") .WithDescription("Get all budgets")
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName()) .WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Budget[]>(StatusCodes.Status200OK) .Produces<Budget[]>(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants); .WithTags(EndpointTags.Grants);
} }
@@ -1,32 +1,36 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)] [ApiVersionTarget(1)]
public class GetContractByIdEndpoint : IEndpoint public class GetContractEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapGet("api/grants/contracts/{contractId}", async (string contractId, GrantsService service, builder.MapGet("api/grants/contracts/{contractId}", async (string contractId, GrantsService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetContractAsync(contractId, cancellationToken) switch
var result = await service.GetContractByIdAsync(contractId, cancellationToken); {
{ IsSuccess: true, Value: var contract } => Results.Ok(contract),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.NotFound(); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get contract by ID") .WithDescription("Get contract by ID")
.WithName(typeof(GetContractByIdEndpoint).ToEndpointName()) .WithName(typeof(GetContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Contract>(StatusCodes.Status200OK) .Produces<Contract>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants); .WithTags(EndpointTags.Grants);
} }
} }
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants; namespace PostFundManagement.Api.Endpoints.Grants;
@@ -15,19 +16,17 @@ public class GetContractsEndpoint : IEndpoint
{ {
builder.MapGet("api/grants/contracts", async (GrantsService service, builder.MapGet("api/grants/contracts", async (GrantsService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetContractsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetContractsAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var contracts } => Results.Ok(contracts),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get all contracts")
}) .WithName(typeof(GetContractsEndpoint).ToEndpointName())
.WithDescription("Get all contracts") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetContractsEndpoint).ToEndpointName()) .Produces<Contract[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Contract[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Grants);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -13,19 +15,20 @@ public class CreateActivityEndpoint : IEndpoint
{ {
builder.MapPost("api/projects/activities", async (CreateActivityRequest request, builder.MapPost("api/projects/activities", async (CreateActivityRequest request,
HttpContext context, ProjectService service, CancellationToken cancellationToken = default) => HttpContext context, ProjectService service, CancellationToken cancellationToken = default) =>
{ await service.CreateActivityAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateActivityAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } result => Results.Ok(),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok() var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Add a new activity under project (M04)") .WithDescription("Add a new activity under project (M04)")
.WithName(typeof(CreateActivityEndpoint).ToEndpointName()) .WithName(typeof(CreateActivityEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects); .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -13,19 +15,22 @@ public class CreateProjectEndpoint : IEndpoint
{ {
builder.MapPost("api/projects", async (CreateProjectRequest request, HttpContext context, builder.MapPost("api/projects", async (CreateProjectRequest request, HttpContext context,
ProjectService service, CancellationToken cancellationToken = default) => ProjectService service, CancellationToken cancellationToken = default) =>
{ await service.CreateProjectAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.CreateProjectAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } result => Results.Ok(),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok() var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
}) var r => Results.InternalServerError(r.Errors.ToProblems())
.WithDescription("Create a new project (M04)") })
.WithName(typeof(CreateProjectEndpoint).ToEndpointName()) .WithDescription("Create a new project (M04)")
.MapToApiVersion(new ApiVersion(1)) .WithName(typeof(CreateProjectEndpoint).ToEndpointName())
.Produces(StatusCodes.Status200OK) .MapToApiVersion(new ApiVersion(1))
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status404NotFound)
.WithTags(EndpointTags.Projects); .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -13,18 +15,19 @@ public class GetActivitiesEndpoint : IEndpoint
{ {
builder.MapGet("api/projects/{projectId}/activities", async (string projectId, builder.MapGet("api/projects/{projectId}/activities", async (string projectId,
ProjectService service, CancellationToken cancellationToken = default) => ProjectService service, CancellationToken cancellationToken = default) =>
{ await service.GetActivitiesAsync(projectId, cancellationToken) switch
var result = await service.GetActivitiesAsync(projectId, cancellationToken); {
{ IsSuccess: true, Value: var activities } => Results.Ok(activities),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.NotFound(); })
}) .WithDescription("Get a list of activities for a project")
.WithDescription("Get a list of activities for a project") .WithName(typeof(GetActivitiesEndpoint).ToEndpointName())
.WithName(typeof(GetActivitiesEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces<Domain.Models.Activity[]>(StatusCodes.Status200OK)
.Produces<Domain.Models.Activity[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects); .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -13,19 +15,21 @@ public class GetActivityEndpoint : IEndpoint
{ {
builder.MapGet("api/projects/activities/{activityId}", async (string activityId, builder.MapGet("api/projects/activities/{activityId}", async (string activityId,
ProjectService service, CancellationToken cancellationToken = default) => ProjectService service, CancellationToken cancellationToken = default) =>
{ await service.GetActivityAsync(activityId, cancellationToken) switch
var result = await service.GetActivityAsync(activityId, cancellationToken); {
{ IsSuccess: true, Value: var activity } => Results.Ok(activity),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.NotFound(); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Gets a specific activity by it's id") .WithDescription("Gets a specific activity by it's id")
.WithName(typeof(GetActivityEndpoint).ToEndpointName()) .WithName(typeof(GetActivityEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Activity>(StatusCodes.Status200OK) .Produces<Domain.Models.Activity>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden)
.WithTags(EndpointTags.Projects); .Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -1,9 +1,11 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -14,20 +16,22 @@ public class GetMilestoneEndpoint : IEndpoint
{ {
builder.MapGet("api/projects/milestones/{milestoneId}", async (string milestoneId, ProjectService service, builder.MapGet("api/projects/milestones/{milestoneId}", async (string milestoneId, ProjectService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetMilestoneAsync(milestoneId, cancellationToken) switch
var result = await service.GetMilestoneAsync(milestoneId, cancellationToken); {
{ IsSuccess: true, Value: var milestone } => Results.Ok(milestone),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.NotFound(); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Gets a specific milestone") .WithDescription("Gets a specific milestone")
.WithName(typeof(GetMilestoneEndpoint).ToEndpointName()) .WithName(typeof(GetMilestoneEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Milestone>(StatusCodes.Status200OK) .Produces<Milestone>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden)
.WithTags(EndpointTags.Projects) .Produces(StatusCodes.Status401Unauthorized)
.CacheOutput(); .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
} }
} }
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -15,20 +16,18 @@ public class GetMilestonesEndpoint : IEndpoint
{ {
builder.MapGet("api/projects/milestones/{page:int}/{pageSize:int}", async (ProjectService service, builder.MapGet("api/projects/milestones/{page:int}/{pageSize:int}", async (ProjectService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetMilestonesAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetMilestonesAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var milestones } => Results.Ok(milestones),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .RequireAuthorization()
}) .WithDescription("Get a list of all milestones")
.RequireAuthorization() .WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
.WithDescription("Get a list of all milestones") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName()) .Produces<Milestone[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Milestone[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Projects);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -1,9 +1,11 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -14,19 +16,21 @@ public class GetProjectEndpoint : IEndpoint
{ {
builder.MapGet("api/projects/{projectId}", async (string projectId, ProjectService service, builder.MapGet("api/projects/{projectId}", async (string projectId, ProjectService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetProjectAsync(projectId, cancellationToken) switch
var result = await service.GetProjectAsync(projectId, cancellationToken); {
{ IsSuccess: true, Value: var project } => Results.Ok(project),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r when r.HasError<NotFoundError>() => Results.NotFound(),
: Results.NotFound(); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get project by ID") .WithDescription("Get project by ID")
.WithName(typeof(GetProjectEndpoint).ToEndpointName()) .WithName(typeof(GetProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<Project[]>(StatusCodes.Status200OK) .Produces<Project[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden)
.WithTags(EndpointTags.Projects); .Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -1,9 +1,11 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -14,19 +16,20 @@ public class GetProjectIndicatorsEndpoint : IEndpoint
{ {
builder.MapGet("api/projects/indicators/project/{projectId}", async (string projectId, ProjectService service, builder.MapGet("api/projects/indicators/project/{projectId}", async (string projectId, ProjectService service,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
{ await service.GetProjectIndicatorsAsync(projectIdHash: projectId, cancellationToken) switch
var result = await service.GetProjectIndicatorsAsync(projectIdHash: projectId, cancellationToken); {
{ IsSuccess: true, Value: var indicators } => Results.Ok(indicators),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.NotFound(); })
}) .WithDescription("Get get a detailed list of project indicators")
.WithDescription("Get get a detailed list of project indicators") .WithName(typeof(GetProjectIndicatorsEndpoint).ToEndpointName())
.WithName(typeof(GetProjectIndicatorsEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces<Indicator[]>(StatusCodes.Status200OK)
.Produces<Indicator[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects); .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -1,9 +1,11 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -14,19 +16,20 @@ public class GetProjectMilestonesEndpoint : IEndpoint
{ {
builder.MapGet("api/projects/{projectId}/milestones", async (string projectId, builder.MapGet("api/projects/{projectId}/milestones", async (string projectId,
ProjectService service, CancellationToken cancellationToken = default) => ProjectService service, CancellationToken cancellationToken = default) =>
{ await service.GetProjectMilestonesAsync(projectId, cancellationToken) switch
var result = await service.GetProjectMilestonesAsync(projectId, cancellationToken); {
{ IsSuccess: true, Value: var milestones } => Results.Ok(milestones),
return result.IsSuccess var r when r.HasError<IdDecodeError>() => Results.Forbid(),
? Results.Ok(result.Value) var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.NotFound(); })
}) .WithDescription("Get a list of milestones for a project")
.WithDescription("Get a list of milestones for a project") .WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName())
.WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces<Milestone[]>(StatusCodes.Status200OK)
.Produces<Milestone[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects); .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public class GetProjectRisksEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId}/risks", async (string projectId,
ProjectService service, CancellationToken cancellationToken = default) =>
await service.GetProjectRisksAsync(projectId, cancellationToken) switch
{
{ IsSuccess: true, Value: var risks } => Results.Ok(risks),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of risks for a project")
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Risk[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -15,19 +16,17 @@ public class GetProjectsEndpoint : IEndpoint
{ {
builder.MapGet("api/projects", async (ProjectService service, builder.MapGet("api/projects", async (ProjectService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetProjectsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
var result = await service.GetProjectsAsync(Pagination.Create(page, pageSize), cancellationToken); {
{ IsSuccess: true, Value: var projects } => Results.Ok(projects),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Get a list of projects")
}) .WithName(typeof(GetProjectsEndpoint).ToEndpointName())
.WithDescription("Get a list of projects") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetProjectsEndpoint).ToEndpointName()) .Produces<Project[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces<Project[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Projects);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -3,6 +3,7 @@ using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -13,19 +14,17 @@ public sealed class UpdateMilestoneEndpoint : IEndpoint
{ {
builder.MapPut("api/projects/milestone", async (UpdateMilestoneRequest request, ProjectService service, builder.MapPut("api/projects/milestone", async (UpdateMilestoneRequest request, ProjectService service,
HttpContext context, CancellationToken cancellationToken = default) => HttpContext context, CancellationToken cancellationToken = default) =>
{ await service.UpdateMilestoneAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.UpdateMilestoneAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok() })
: Results.BadRequest(result.Errors.ToProblems()); .WithDescription("Update project milestone details")
}) .WithName(typeof(UpdateMilestoneEndpoint).ToEndpointName())
.WithDescription("Update project milestone details") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(UpdateMilestoneEndpoint).ToEndpointName()) .Produces(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .WithTags(EndpointTags.Projects);
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -13,18 +15,18 @@ public sealed class UpdateProjectEndpoint : IEndpoint
{ {
builder.MapPut("api/projects", async (UpdateProjectRequest request, builder.MapPut("api/projects", async (UpdateProjectRequest request,
ProjectService service, HttpContext context, CancellationToken cancellationToken = default) => ProjectService service, HttpContext context, CancellationToken cancellationToken = default) =>
{ await service.UpdateProjectAsync(request, userId: context.GetUserId(), cancellationToken) switch
var result = await service.UpdateProjectAsync(request, userId: context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
? Results.Ok() var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.BadRequest(result.Errors.ToProblems()); })
})
.WithDescription("Update project details") .WithDescription("Update project details")
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName()) .WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects); .WithTags(EndpointTags.Projects);
} }
@@ -1,8 +1,10 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects; namespace PostFundManagement.Api.Endpoints.Projects;
@@ -13,19 +15,20 @@ public class UpdateProjectStatusEndpoint : IEndpoint
{ {
builder.MapPut("api/projects/status", async (UpdateProjectStatusRequest request, HttpContext context, builder.MapPut("api/projects/status", async (UpdateProjectStatusRequest request, HttpContext context,
ProjectService service, CancellationToken cancellationToken = default) => ProjectService service, CancellationToken cancellationToken = default) =>
{ await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken) switch
var result = await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok() var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
: Results.BadRequest(result.Errors.ToProblems()); var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update project status (M04)") .WithDescription("Update project status (M04)")
.WithName(typeof(UpdateProjectStatusEndpoint).ToEndpointName()) .WithName(typeof(UpdateProjectStatusEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status401Unauthorized) .Produces<Problems>(StatusCodes.Status400BadRequest)
.WithTags(EndpointTags.Projects); .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
} }
} }
@@ -1,9 +1,11 @@
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Users; using PostFundManagement.Application.Services.Users;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Users; namespace PostFundManagement.Api.Endpoints.Users;
@@ -14,19 +16,19 @@ public sealed class GetUserEndpoint : IEndpoint
{ {
builder.MapGet("api/users/{userId:Guid}", async (Guid userId, UserService service, builder.MapGet("api/users/{userId:Guid}", async (Guid userId, UserService service,
HttpContext context, CancellationToken cancellationToken = default) => HttpContext context, CancellationToken cancellationToken = default) =>
{ await service.GetUserAsync(userId, cancellationToken) switch
var result = await service.GetUserAsync(userId, cancellationToken); {
{ IsSuccess: true, Value: var user } => Results.Ok(user),
return result.IsSuccess var r when r.HasError<NotFoundError>() => Results.NotFound(),
? Results.Ok(result.Value) var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.NotFound(); })
}) .WithDescription("Gets the metadata of a single user.")
.WithDescription("Gets the metadata of a single user.") .WithName(typeof(GetUserEndpoint).ToEndpointName())
.WithName(typeof(GetUserEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces<User>(StatusCodes.Status200OK)
.Produces<User>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status401Unauthorized) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Users); .WithTags(EndpointTags.Users);
} }
} }
@@ -5,6 +5,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Users; namespace PostFundManagement.Api.Endpoints.Users;
@@ -15,19 +16,18 @@ public class GetUsersEndpoint : IEndpoint
{ {
builder.MapPost("api/users/{page:int}/{pageSize:int}", async (UserFilterRequest request, UserService service, builder.MapPost("api/users/{page:int}/{pageSize:int}", async (UserFilterRequest request, UserService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
{ await service.GetUsersAsync(Pagination.Create(page, pageSize), request, cancellationToken) switch
var result = await service.GetUsersAsync(Pagination.Create(page, pageSize), request, cancellationToken); {
{ IsSuccess: true, Value: var users } => Results.Ok(users),
return result.IsSuccess var r => Results.InternalServerError(r.Errors.ToProblems())
? Results.Ok(result.Value) })
: Results.NotFound(); .WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
}) .WithName(typeof(GetUsersEndpoint).ToEndpointName())
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting") .MapToApiVersion(new ApiVersion(1))
.WithName(typeof(GetUsersEndpoint).ToEndpointName()) .Produces<User[]>(StatusCodes.Status200OK)
.MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status404NotFound)
.Produces<User[]>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Users);
.WithTags(EndpointTags.Users);
} }
} }
@@ -2,8 +2,10 @@ using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Communications.Events; using PostFundManagement.Application.Services.Communications.Events;
using PostFundManagement.Application.Services.Users; using PostFundManagement.Application.Services.Users;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Users; namespace PostFundManagement.Api.Endpoints.Users;
@@ -20,15 +22,19 @@ public class SyncUserEndpoint : IEndpoint
if (result.IsSuccess) if (result.IsSuccess)
await jobOrchestrator.SendAsync(SyncUserEvent.Create(), cancellationToken); await jobOrchestrator.SendAsync(SyncUserEvent.Create(), cancellationToken);
return result.IsSuccess return result switch
? Results.Ok() {
: Results.BadRequest(result.Errors.ToProblems()); { IsSuccess: true } => Results.Ok(),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
};
}) })
.WithDescription("Trigger integration and data synchronization in the background") .WithDescription("Trigger integration and data synchronization in the background")
.WithName(typeof(SyncUserEndpoint).ToEndpointName()) .WithName(typeof(SyncUserEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Users); .WithTags(EndpointTags.Users);
} }
} }
@@ -2,8 +2,10 @@
using PostFundManagement.Application.Services.Users; using PostFundManagement.Application.Services.Users;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Users; namespace PostFundManagement.Api.Endpoints.Users;
@@ -14,19 +16,20 @@ public sealed class UpdateUserStatusEndpoint : IEndpoint
{ {
builder.MapGet("api/users/{userId:Guid}/status/{status:int}", async (Guid userId, ActivityStatus status, builder.MapGet("api/users/{userId:Guid}/status/{status:int}", async (Guid userId, ActivityStatus status,
UserService service, CancellationToken cancellationToken = default) => UserService service, CancellationToken cancellationToken = default) =>
{ await service.UpdateUserStatusAsync(userId, status, cancellationToken) switch
var result = await service.UpdateUserStatusAsync(userId, status, cancellationToken); {
{ IsSuccess: true } => Results.Ok(),
return result.IsSuccess var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
? Results.Ok() var r => Results.InternalServerError(r.Errors.ToProblems())
: Results.NotFound(); })
}) .WithDescription("Changes the Activity status of the user in the system")
.WithDescription("Changes the Activity status of the user in the system") .WithName(typeof(UpdateUserStatusEndpoint).ToEndpointName())
.WithName(typeof(UpdateUserStatusEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1))
.MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status401Unauthorized) .Produces<Problems>(StatusCodes.Status400BadRequest)
.WithTags(EndpointTags.Users); .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Users);
} }
} }
@@ -1,4 +1,5 @@
using PostFundManagement.Application.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain.Errors;
namespace PostFundManagement.Application.Abstractions; namespace PostFundManagement.Application.Abstractions;
@@ -9,17 +10,16 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
protected abstract string BucketName { get; } protected abstract string BucketName { get; }
protected abstract string CdnBaseUrl { get; } protected abstract string CdnBaseUrl { get; }
public virtual async Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) public virtual async ValueTask<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
try
{ {
if (string.IsNullOrWhiteSpace(BucketName)) 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)) 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.CopyToAsync(stream, cancellationToken);
await fileStream.DisposeAsync(); await fileStream.DisposeAsync();
@@ -28,8 +28,8 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
var fileHash = HashService.StreamToSha256Hash(stream); var fileHash = HashService.StreamToSha256Hash(stream);
if(string.IsNullOrWhiteSpace(fileHash)) if (string.IsNullOrWhiteSpace(fileHash))
return Result.Fail<string>("Failed to compute file hash."); return Result.Fail<string>(CreateError.Create("Failed to compute file hash."));
var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}"; 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); var response = await Client.PutObjectAsync(putRequest, cancellationToken);
return response.HttpStatusCode != System.Net.HttpStatusCode.OK return response.HttpStatusCode != HttpStatusCode.OK
? Result.Fail<string>($"Failed to upload {fileName} to S3.") ? Result.Fail<string>(CreateError.Create($"Failed to upload {fileName} to S3."))
: Result.Ok($"{CdnBaseUrl}/{fileKey}"); : 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) public virtual async ValueTask<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
if (string.IsNullOrWhiteSpace(BucketName)) 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, BucketName = BucketName,
Key = fileKey Key = fileKey
}; }, cancellationToken);
var response = await Client.DeleteObjectAsync(deleteRequest, cancellationToken); return Result.FailIf(response.HttpStatusCode == HttpStatusCode.NoContent,
UpdateError.Create($"Failed to delete {fileKey} from S3."));
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));
}
}
} }
@@ -1,4 +1,3 @@
using Asp.Versioning;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using PostFundManagement.Domain.Security.Configuration; 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 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) 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; using PostFundManagement.Domain.Configuration.Hash;
namespace PostFundManagement.Application.Extensions; 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); return (object?)Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
context.HttpContext.Items[UserIdItemKey] = userId; context.HttpContext.Items[UserIdItemKey] = userId;
context.HttpContext.Items[EmaiItemKey] = email; context.HttpContext.Items[EmailtemKey] = email;
return await next(context); return await next(context);
} }
} }
@@ -1,5 +1,6 @@
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
@@ -8,11 +9,10 @@ namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
{ {
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var invite = new Domain.Entities.Invite var invite = new Domain.Entities.Invite
{ {
@@ -40,19 +40,13 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(invite.Map()) ? Result.Ok(invite.Map())
: Result.Fail("Failed to complete the insite distribution"); : Result.Fail<Invite>(CreateError.New);
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var invites = await context.Invites.AsNoTracking() var invites = await context.Invites.AsNoTracking()
.OrderByDescending(i => i.CreatedAt) .OrderByDescending(i => i.CreatedAt)
@@ -60,29 +54,18 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(invites?.Select(i => i.Map()).ToArray() ?? []); return Result.Ok(invites.Select(i => i.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken) public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken) =>
{ await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
try
{ {
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); var invite = await context.Invites.AsNoTracking().FirstOrDefaultAsync(i => i.Recipient == userId, cancellationToken);
return invite is not null return invite is not null
? Result.Ok(invite.Map()) ? Result.Ok(invite.Map())
: Result.Fail("Failed to find specific invite."); : Result.Fail<Invite>(NotFoundError.New);
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -1,35 +1,34 @@
using PostFundManagement.Application.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Evidence; 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 [FromKeyedServices(Constants.EvidenceBucketName)] IS3Service s3Service) : IService
{ {
public async ValueTask<Result> UploadEvidenceFileAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) public async ValueTask<Result> UploadEvidenceFileAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try {
{ if (!long.TryParse(form["projectId"], out var projectId) || !long.TryParse(form["milestoneId"], out var milestoneId) ||
_ = long.TryParse(form["projectId"], out var projectId); !long.TryParse(form["indicatorId"], out var indicatorId) || !long.TryParse(form["activityId"], out var activityId) ||
_ = long.TryParse(form["milestoneId"], out var milestoneId); !Enum.TryParse<EvidenceType>(form["type"], out var type))
_ = long.TryParse(form["indicatorId"], out var indicatorId); return Result.Fail(CreateError.Create("Invalid or missing form data parameters."));
_ = long.TryParse(form["activityId"], out var activityId);
_ = Enum.TryParse<EvidenceType>(form["type"], out var type);
using var stream = file.OpenReadStream(); await using var stream = file.OpenReadStream();
var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken); var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken);
if (uploadResult.IsFailed) if (uploadResult.IsFailed)
return Result.Fail(uploadResult.Errors.FirstOrDefault()?.Message ?? "File upload failed."); return Result.Fail(uploadResult.Errors);
var documentUrl = uploadResult.Value; 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 var evidence = new Domain.Entities.Evidence
{ {
@@ -47,47 +46,32 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
context.Evidences.Add(evidence); context.Evidences.Add(evidence);
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Evidence upload failed");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try {
{
var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash); var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash);
if (decodeResult.IsFailed) 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 .ExecuteUpdateAsync(f => f
.SetProperty(f => f.ActualAmount, amount) .SetProperty(f => f.ActualAmount, amount)
.SetProperty(f => f.CreatedBy, userId), cancellationToken); .SetProperty(f => f.CreatedBy, userId), cancellationToken);
return totalUpdated > 0 return Result.FailIf(changedTotal <= 0, UpdateError.New);
? Result.Ok() }));
: Result.Fail("Failed to record actual amount");
} public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
catch (Exception ex) await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
{ {
return Result.Fail(new Error(ex.Message).CausedBy(ex)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
}
}
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var indicators = await context.Indicators.AsNoTracking() var indicators = await context.Indicators.AsNoTracking()
.OrderByDescending(i => i.CreatedAt) .OrderByDescending(i => i.CreatedAt)
@@ -95,19 +79,13 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(indicators?.Select(i => i.Map()).ToArray() ?? []); return Result.Ok(indicators.Select(i => i.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var evidence = await context.Evidences.AsNoTracking() var evidence = await context.Evidences.AsNoTracking()
.OrderByDescending(e => e.CreatedAt) .OrderByDescending(e => e.CreatedAt)
@@ -115,22 +93,16 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(evidence?.Select(i => i.Map()).ToArray() ?? []); return Result.Ok(evidence.Select(i => i.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
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)) 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 context.Milestones.Add(new Domain.Entities.Milestone
{ {
@@ -143,27 +115,16 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create the milestone");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await 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");
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, 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 context.Indicators.Add(new Domain.Entities.Indicator
{ {
@@ -177,72 +138,55 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create the invite");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) =>
{ await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(indicatorId); var decodeResult = hashService.DecodeLongIdHash(indicatorId);
if (decodeResult.IsFailed) 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); var indicator = await context.Indicators.AsNoTracking().FirstOrDefaultAsync(i => i.Id == decodeResult.Value, cancellationToken);
return indicator is not null return indicator is not null
? Result.Ok(indicator.Map()) ? Result.Ok(indicator.Map())
: Result.Fail("Failed to find specific indicator."); : Result.Fail(NotFoundError.New);
} }));
catch (Exception ex)
{ public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
return Result.Fail(new Error(ex.Message).CausedBy(ex)); await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence>>>)(async () =>
}
}
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default)
{
try
{ {
var decodeResult = hashService.DecodeLongIdHash(evidenceId); var decodeResult = hashService.DecodeLongIdHash(evidenceId);
if (decodeResult.IsFailed) 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); var evidence = await context.Evidences.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken);
return evidence is not null return evidence is not null
? Result.Ok(evidence.Map()) ? Result.Ok(evidence.Map())
: Result.Fail("Failed to get a specific evidence record."); : Result.Fail<Domain.Models.Evidence>(NotFoundError.New);
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash);
if (decodeResult.IsFailed) 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() var evidences = await context.Evidences.AsNoTracking()
.Where(e => e.MilestoneId == decodeResult.Value) .Where(e => e.MilestoneId == decodeResult.Value)
@@ -250,24 +194,19 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.ThenByDescending(o => o.UpdatedAt) .ThenByDescending(o => o.UpdatedAt)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); return Result.Ok(evidences.Select(e => e.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async Task<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) public async ValueTask<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed) 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() var evidences = await context.Evidences.AsNoTracking()
.Where(e => e.ProjectId == decodeResult.Value) .Where(e => e.ProjectId == decodeResult.Value)
@@ -275,11 +214,6 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.ThenBy(o => o.UpdatedAt) .ThenBy(o => o.UpdatedAt)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); return Result.Ok(evidences.Select(e => e.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -1,6 +1,7 @@
using PostFundManagement.Application.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
@@ -9,11 +10,10 @@ namespace PostFundManagement.Application.Services.Execution;
public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
{ {
public async Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var risks = await context.Risks.AsNoTracking() var risks = await context.Risks.AsNoTracking()
.OrderByDescending(r => r.CreatedAt) .OrderByDescending(r => r.CreatedAt)
@@ -21,66 +21,30 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(risks?.Select(i => i.Map()).ToArray() ?? []); return Result.Ok(risks.Select(i => i.Map()).ToArray());
} }));
catch (Exception ex)
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)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
}
}
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);
var disbursements = await context.Disbursements.AsNoTracking() var disbursements = await context.Disbursements.AsNoTracking()
.OrderByDescending(d => d.CreatedAt) .OrderByDescending(d => d.CreatedAt)
.Skip((pagination.Page - 1) * pagination.PageSize) .Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToArrayAsync(cancellationToken);
return Result.Ok(disbursements?.Select(i => i.Map()).ToArray() ?? []); return Result.Ok(disbursements.Select(i => i.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
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)) 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 context.Risks.Add(new Domain.Entities.Risk
{ {
@@ -93,24 +57,16 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create project risk");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
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)) 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 context.Disbursements.Add(new Domain.Entities.Disbursement
{ {
@@ -122,40 +78,96 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create disbursement");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try {
{
var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash); var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash);
if (decodeResult.IsFailed) 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 => .ExecuteUpdateAsync(f =>
f.SetProperty(f => f.Status, ApprovalStatus.Approved) f.SetProperty(f => f.Status, ApprovalStatus.Approved)
.SetProperty(f => f.UpdatedBy, userId) .SetProperty(f => f.UpdatedBy, userId)
.SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken); .SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken);
return totalSaved > 0 return Result.FailIf(changedTotal <= 0, UpdateError.New);
? Result.Ok() }));
: Result.Fail("Failed to approve payment");
} public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken) =>
catch (Exception ex) await Result.Try((Func<ValueTask<Result<Disbursement>>>)(async () =>
{ {
return Result.Fail(new Error(ex.Message).CausedBy(ex)); 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; 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 CreateDisbursementRequest(long ProjectId, long? MilestoneId, decimal Amount);
public record CreateRiskRequest(long ProjectId, RiskLikelihood Likelihood, RiskImpact Impact, string Name, string Description); 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;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
@@ -9,43 +10,30 @@ namespace PostFundManagement.Application.Services.Governance;
public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
{ {
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(CancellationToken cancellationToken = default) public async ValueTask<Result<Programme[]>> GetProgrammesAsync(CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var programmes = await context.Programmes.AsNoTracking().ToListAsync(cancellationToken); var programmes = await context.Programmes.AsNoTracking().ToListAsync(cancellationToken);
return Result.Ok(programmes?.Select(p => p.Map()).ToArray() ?? []); return Result.Ok(programmes.Select(p => p.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(CancellationToken cancellationToken = default) public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var portfolios = await context.Portfolios.AsNoTracking().ToListAsync(cancellationToken); var portfolios = await context.Portfolios.AsNoTracking().ToListAsync(cancellationToken);
return Result.Ok(portfolios?.Select(p => p.Map()).ToArray() ?? []); return Result.Ok(portfolios.Select(p => p.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Organisation[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var organisations = await context.Organisations.AsNoTracking() var organisations = await context.Organisations.AsNoTracking()
.OrderBy(o => o.Name) .OrderBy(o => o.Name)
@@ -53,59 +41,42 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(organisations?.Select(o => o.Map()).ToArray() ?? []); return Result.Ok(organisations.Select(o => o.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Organisation>> GetOrganisationByIdAsync(string organisationIdHash, CancellationToken cancellationToken = default) public async ValueTask<Result<Organisation>> GetOrganisationByIdAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(organisationIdHash); var decodeResult = hashService.DecodeLongIdHash(organisationIdHash);
if (decodeResult.IsFailed) 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() var organisation = await context.Organisations.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken); .FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken);
return organisation is not null return organisation is not null
? Result.Ok(organisation.Map()) ? Result.Ok(organisation.Map())
: Result.Fail("Failed to find specific organisation"); : Result.Fail(NotFoundError.New);
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(CancellationToken cancellationToken = default) public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var instruments = await context.Instruments.AsNoTracking().ToListAsync(cancellationToken); var instruments = await context.Instruments.AsNoTracking().ToListAsync(cancellationToken);
return Result.Ok(instruments?.Select(i => i.Map()).ToArray() ?? []); return Result.Ok(instruments.Select(i => i.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Dashboard>>>)(async () =>
try
{ {
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 totalPortfolioValue = await context.Awards.SumAsync(a => a.Amount, cancellationToken);
var activeAwardsCount = await context.Awards.CountAsync(a => a.Status == ApprovalStatus.Approved, 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, return Result.Ok(new Dashboard(totalPortfolioValue, activeAwardsCount,
disbursedAmount, onTrackPercentage, atRiskCount, beneficiariesReached)); 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) public async ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try {
{ await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Organisations.AnyAsync(o => o.RegistrationNo == request.RegistrationNo, 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 context.Organisations.Add(new Domain.Entities.Organisation
{ {
@@ -153,13 +118,6 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create organisation");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -1,6 +1,7 @@
using PostFundManagement.Application.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
@@ -9,11 +10,10 @@ namespace PostFundManagement.Application.Services.Grants;
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
{ {
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contracts = await context.Contracts.AsNoTracking() var contracts = await context.Contracts.AsNoTracking()
.OrderByDescending(c => c.CreatedAt) .OrderByDescending(c => c.CreatedAt)
@@ -21,42 +21,32 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(contracts?.Select(c => c.Map()).ToArray() ?? []); return Result.Ok(contracts.Select(c => c.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Contract>> GetContractByIdAsync(string contractIdHash, CancellationToken cancellationToken = default) public async ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Contract>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(contractIdHash); var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
if (decodeResult.IsFailed) 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 return contract is not null
? Result.Ok(contract.Map()) ? Result.Ok(contract.Map())
: Result.Fail("Failed to find specific contract"); : Result.Fail<Contract>(NotFoundError.New);
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Budget[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var budgets = await context.Budgets.AsNoTracking() var budgets = await context.Budgets.AsNoTracking()
.OrderByDescending(b => b.CreatedAt) .OrderByDescending(b => b.CreatedAt)
@@ -64,42 +54,32 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(budgets?.Select(b => b.Map()).ToArray() ?? []); return Result.Ok(budgets.Select(b => b.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Budget>> GetBudgetByIdAsync(string budgetIdHash, CancellationToken cancellationToken = default) public async ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Budget>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(budgetIdHash); var decodeResult = hashService.DecodeLongIdHash(budgetIdHash);
if (decodeResult.IsFailed) 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 return budget is not null
? Result.Ok(budget.Map()) ? Result.Ok(budget.Map())
: Result.Fail("Failed to get specific budget."); : Result.Fail<Budget>(NotFoundError.New);
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Award[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var awards = await context.Awards.AsNoTracking() var awards = await context.Awards.AsNoTracking()
.OrderByDescending(a => a.CreatedAt) .OrderByDescending(a => a.CreatedAt)
@@ -107,42 +87,31 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(awards?.Select(a => a.Map()).ToArray() ?? []); return Result.Ok(awards.Select(a => a.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Award>> GetAwardByIdAsync(string awardIdHash, CancellationToken cancellationToken = default) public async ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Award>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(awardIdHash); var decodeResult = hashService.DecodeLongIdHash(awardIdHash);
if (decodeResult.IsFailed) 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); var award = await context.Awards.AsNoTracking().FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken);
return award is not null return award is not null
? Result.Ok(award.Map()) ? Result.Ok(award.Map())
: Result.Fail("Award not found"); : Result.Fail<Award>(NotFoundError.New);
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Contracts var changedTotal = await context.Contracts
.Where(c => c.Id == request.ContractId && c.Status != ContractStatus.Active) .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.CreatedBy, userId)
.SetProperty(u => u.Status, ContractStatus.Active), cancellationToken); .SetProperty(u => u.Status, ContractStatus.Active), cancellationToken);
return changedTotal > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, UpdateError.New);
? Result.Ok() }));
: Result.Fail("Contract activation failed");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var award = await context.Awards.AsNoTracking() var award = await context.Awards.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == request.AwardId, cancellationToken); .FirstOrDefaultAsync(a => a.Id == request.AwardId, cancellationToken);
if (award == null) 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 var contract = new Domain.Entities.Contract
{ {
@@ -187,27 +148,19 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
context.Contracts.Add(contract); context.Contracts.Add(contract);
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create contract");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
if (request.Amount <= 0) 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)) 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 var budget = new Domain.Entities.Budget
{ {
@@ -220,30 +173,22 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
context.Budgets.Add(budget); context.Budgets.Add(budget);
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create budget");
} public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
catch (Exception ex) 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) 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)) 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)) 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 var award = new Domain.Entities.Award
{ {
@@ -257,13 +202,6 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
context.Awards.Add(award); context.Awards.Add(award);
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create award.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -1,6 +1,7 @@
using PostFundManagement.Application.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
@@ -9,16 +10,15 @@ namespace PostFundManagement.Application.Services.Projects;
public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
{ {
public async ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
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); var project = await context.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, cancellationToken);
if (project == null) if (project == null)
return Result.Fail("Project does not exist."); return Result.Fail(NotFoundError.New);
project.Status = request.Status; project.Status = request.Status;
project.UpdatedBy = userId; project.UpdatedBy = userId;
@@ -33,93 +33,69 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.SetProperty(p => p.UpdatedBy, userId) .SetProperty(p => p.UpdatedBy, userId)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken); .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
return changedTotal > 0 return Result.FailIf(changedTotal == 0, UpdateError.New);
? Result.Ok() }));
: Result.Fail("Failed to update project");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
try
{ {
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) var projects = await context.Projects.AsNoTracking().OrderByDescending(p => p.CreatedAt)
.Skip((pagination.Page - 1) * pagination.PageSize) .Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(projects?.Select(p => p.Map()).ToArray() ?? []); return Result.Ok(projects.Select(p => p.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Project>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed) 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 return project is not null
? Result.Ok(project.Map()) ? Result.Ok(project.Map())
: Result.Fail("Failed to get specific project."); : Result.Fail<Project>(NotFoundError.Create());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Domain.Models.Activity[]>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed) 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() var activities = await context.Activities.AsNoTracking()
.Where(a => a.ProjectId == decodeResult.Value) .Where(a => a.ProjectId == decodeResult.Value)
.OrderByDescending(a => a.CreatedAt) .OrderByDescending(a => a.CreatedAt)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(activities?.Select(a => a.Map()).ToArray() ?? []); return Result.Ok(activities.Select(a => a.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); 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)) 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)) 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 context.Projects.Add(new Domain.Entities.Project
{ {
@@ -133,28 +109,20 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create project");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try {
{ await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, 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 (request.MilestoneId.HasValue)
if (!await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken)) 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 context.Activities.Add(new Domain.Entities.Activity
{ {
@@ -170,93 +138,69 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to create activity.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed) 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() var milestones = await context.Milestones.AsNoTracking()
.Where(m => m.ProjectId == decodeResult.Value) .Where(m => m.ProjectId == decodeResult.Value)
.OrderBy(m => m.DueAt) .OrderBy(m => m.DueAt)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); return Result.Ok(milestones.Select(i => i.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) public async ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) =>
{ await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed) 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() var evidences = await context.Indicators.AsNoTracking()
.Where(e => e.ProjectId == decodeResult.Value) .Where(e => e.ProjectId == decodeResult.Value)
.OrderByDescending(o => o.Id) .OrderByDescending(o => o.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); return Result.Ok(evidences.Select(e => e.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken) public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken) =>
{ await Result.Try((Func<ValueTask<Result<Milestone>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash);
if (decodeResult.IsFailed) 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 return milestone is not null
? Result.Ok(milestone.Map()) ? Result.Ok(milestone.Map())
: Result.Fail("Failed to get specific milestone"); : Result.Fail<Milestone>(NotFoundError.Create());
}));
} public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
catch (Exception ex) await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
{ {
return Result.Fail(new Error(ex.Message).CausedBy(ex)); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
}
}
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var milestones = await context.Milestones.AsNoTracking() var milestones = await context.Milestones.AsNoTracking()
.OrderByDescending(m => m.CreatedAt) .OrderByDescending(m => m.CreatedAt)
@@ -264,19 +208,13 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); return Result.Ok(milestones.Select(i => i.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken) public async ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try {
{ await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Milestones.Where(m => m.Id == request.MilestoneId) var changedTotal = await context.Milestones.Where(m => m.Id == request.MilestoneId)
.ExecuteUpdateAsync(f => f .ExecuteUpdateAsync(f => f
@@ -287,21 +225,13 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.SetProperty(u => u.UpdatedBy, userId) .SetProperty(u => u.UpdatedBy, userId)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
return changedTotal > 0 return Result.FailIf(changedTotal <= 0, UpdateError.New);
? Result.Ok() }));
: Result.Fail("Failed to update milestone");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken) public async ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
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) var changedTotal = await context.Projects.Where(p => p.Id == request.ProjectId)
.ExecuteUpdateAsync(f => f .ExecuteUpdateAsync(f => f
@@ -314,36 +244,44 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.SetProperty(u => u.Sector, request.Sector) .SetProperty(u => u.Sector, request.Sector)
.SetProperty(u => u.ProjectType, request.ProjectType), cancellationToken); .SetProperty(u => u.ProjectType, request.ProjectType), cancellationToken);
return changedTotal > 0 return Result.FailIf(changedTotal <= 0, UpdateError.New);
? Result.Ok() }));
: Result.Fail("Failed to update project");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken) public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken) =>
{ await Result.Try((Func<ValueTask<Result<Domain.Models.Activity>>>)(async () =>
try
{ {
var decodeResult = hashService.DecodeLongIdHash(activityIdHash); var decodeResult = hashService.DecodeLongIdHash(activityIdHash);
if (decodeResult.IsFailed) 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 return activity is not null
? Result.Ok(activity.Map()) ? Result.Ok(activity.Map())
: Result.Fail("Failed to get specific activity."); : Result.Fail<Domain.Models.Activity>(NotFoundError.New);
} }));
catch (Exception ex)
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; namespace PostFundManagement.Application.Services.Shared;
public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage) public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
{ {
public async ValueTask<Result> DeleteAsync(string key) public async ValueTask<Result> DeleteAsync(string key) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
await storage.DeleteAsync(key); await storage.DeleteAsync(key);
return Result.Ok(); return Result.Ok();
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> SaveAsync(string key, string value) public async ValueTask<Result> SaveAsync(string key, string value) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
await storage.SetAsync(key, value); await storage.SetAsync(key, value);
return Result.Ok(); 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 public async ValueTask<Result> SaveAsync<TValue>(string key, TValue value) where TValue : class =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
await storage.SetAsync(key, value); await storage.SetAsync(key, value);
return Result.Ok(); return Result.Ok();
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<string>> GetAsync(string key) public async ValueTask<Result<string>> GetAsync(string key) =>
{ await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
try
{ {
var retrieval = await storage.GetAsync<string>(key); var retrieval = await storage.GetAsync<string>(key);
return retrieval.Success && !string.IsNullOrWhiteSpace(retrieval.Value) return retrieval.Success && !string.IsNullOrWhiteSpace(retrieval.Value)
? Result.Ok(retrieval.Value) ? Result.Ok(retrieval.Value)
: Result.Fail($"Could not find object by key {key}"); : Result.Fail(NotFoundError.Create($"Could not find object by key {key}"));
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<TValue>> GetAsync<TValue>(string key) where TValue : class public async ValueTask<Result<TValue>> GetAsync<TValue>(string key) where TValue : class =>
{ await Result.Try((Func<ValueTask<Result<TValue>>>)(async () =>
try
{ {
var retrieval = await storage.GetAsync<TValue>(key); var retrieval = await storage.GetAsync<TValue>(key);
return retrieval.Success && retrieval.Value is not null return retrieval.Success && retrieval.Value is not null
? Result.Ok(retrieval.Value) ? Result.Ok(retrieval.Value)
: Result.Fail($"Could not find object by key {key}"); : Result.Fail(NotFoundError.Create($"Could not find object by key {key}"));
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -7,6 +7,6 @@ namespace PostFundManagement.Application.Services.Shared;
public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractBucketName)] IAmazonS3 amazonS3) : public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractBucketName)] IAmazonS3 amazonS3) :
S3ServiceBase(amazonS3), IS3Service S3ServiceBase(amazonS3), IS3Service
{ {
protected override string BucketName => configuration.GetSection($"{ContractS3SettingsSection}:BucketName").Value ?? ""; protected override string BucketName => configuration.GetSection($"{ContractS3SettingsSection}:BucketName").Value ?? string.Empty;
protected override string CdnBaseUrl => configuration.GetSection($"{ContractS3SettingsSection}:CdnBaseUrl").Value ?? ""; 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) : public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceBucketName)] IAmazonS3 amazonS3) :
S3ServiceBase(amazonS3), IS3Service S3ServiceBase(amazonS3), IS3Service
{ {
protected override string BucketName => configuration.GetSection($"{EvidenceS3SettingsSection}:BucketName").Value ?? ""; protected override string BucketName => configuration.GetSection($"{EvidenceS3SettingsSection}:BucketName").Value ?? string.Empty;
protected override string CdnBaseUrl => configuration.GetSection($"{EvidenceS3SettingsSection}:CdnBaseUrl").Value ?? ""; 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) : public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralBucketName)] IAmazonS3 amazonS3) :
S3ServiceBase(amazonS3), IS3Service S3ServiceBase(amazonS3), IS3Service
{ {
protected override string BucketName => configuration.GetSection($"{GeneralS3SettingsSection}:BucketName").Value ?? ""; protected override string BucketName => configuration.GetSection($"{GeneralS3SettingsSection}:BucketName").Value ?? string.Empty;
protected override string CdnBaseUrl => configuration.GetSection($"{GeneralS3SettingsSection}:CdnBaseUrl").Value ?? ""; protected override string CdnBaseUrl => configuration.GetSection($"{GeneralS3SettingsSection}:CdnBaseUrl").Value ?? string.Empty;
} }
@@ -1,6 +1,7 @@
using PostFundManagement.Domain.Abstractions; 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 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) public static Result<string> ToMd5Hash(string input)
{ {
if (string.IsNullOrEmpty(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)); byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
} }
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input) 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)); : Result.Ok(hasher.EncodeHex(input));
public Result<string> HashEncodeIntId(int id) => id < 0 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)); : Result.Ok(hasher.Encode(id));
public Result<string> HashEncodeLongId(long id) => id < 0 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)); : Result.Ok(hasher.EncodeLong(id));
public Result<int> DecodeIntIdHash(string hash) 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); 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) 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); 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) public async Task<Result<string>> DecodeHexHash(string hex) =>
{ await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
try
{ {
string decoded = hasher.DecodeHex(hex); string decoded = hasher.DecodeHex(hex);
return string.IsNullOrEmpty(decoded) 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); : 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;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Shared; namespace PostFundManagement.Application.Services.Shared;
public sealed class IdentityService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService public sealed class IdentityService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
{ {
public async ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) public async ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try {
{ await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
return !await context.Users.AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken) var userExistsAndActive = await context.Users
? Result.Fail("Invalid user") .AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken);
: Result.Ok();
} return Result.FailIf(!userExistsAndActive, NotFoundError.Create());
catch (Exception ex) }));
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -1,3 +1,4 @@
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Security.Configuration; using PostFundManagement.Domain.Security.Configuration;
using PostFundManagement.Domain.Security.Models; using PostFundManagement.Domain.Security.Models;
using PostFundManagement.Domain.Security.Sdk; using PostFundManagement.Domain.Security.Sdk;
@@ -8,24 +9,21 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
{ {
private readonly SecurityClientSettings clientSettings = clientOptions.Value; private readonly SecurityClientSettings clientSettings = clientOptions.Value;
public async Task<Result<Domain.Security.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default) public async ValueTask<Result<Domain.Security.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
try
{ {
var request = new Domain.Security.Models.TokenRequest using var response = await connectApi.GetToken(new Domain.Security.Models.TokenRequest
{ {
ClientId = clientSettings.ClientId, ClientId = clientSettings.ClientId,
ClientSecret = clientSettings.ClientSecret, ClientSecret = clientSettings.ClientSecret,
GrantType = clientSettings.GrantType, GrantType = clientSettings.GrantType,
Scope = clientSettings.Scope, Scope = clientSettings.Scope,
}; }, cancellationToken);
using var response = await connectApi.GetToken(request, cancellationToken);
var contentRaw = await response.Content.ReadAsStringAsync(cancellationToken); var contentRaw = await response.Content.ReadAsStringAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(contentRaw)) 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) if (response.IsSuccessStatusCode)
{ {
@@ -33,34 +31,15 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken) return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken)
? Result.Ok(tokenResponse) ? 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); var errorResult = JsonSerializer.Deserialize<TokenErrorResponse>(contentRaw);
if (errorResult != null) return Result.FailIf(errorResult != null,
{ CreateError.Create($"{errorResult!.Error}: {errorResult.ErrorDescription}"));
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));
}
}
} }
@@ -1,5 +1,6 @@
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
@@ -8,11 +9,10 @@ namespace PostFundManagement.Application.Services.Users;
public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
{ {
public async ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default) public async ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
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); 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); context.Users.Update(user);
} }
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
? Result.Ok() }));
: Result.Fail("Failed to sync user");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result<User[]>>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Users.AsNoTracking(); var query = context.Users.AsNoTracking();
@@ -67,54 +59,34 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
else else
query = query.OrderBy(u => u.Id); query = query.OrderBy(u => u.Id);
var totalCount = await query.CountAsync(cancellationToken);
var users = await query.Skip((pagination.Page - 1) * pagination.PageSize) var users = await query.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize).ToListAsync(cancellationToken); .Take(pagination.PageSize).ToListAsync(cancellationToken);
return Result.Ok(users?.Select(u => u.Map()).ToArray() ?? []); return Result.Ok(users.Select(u => u.Map()).ToArray());
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) public async ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) =>
{ await Result.Try((Func<ValueTask<Result>>)(async () =>
try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Users var changedTotal = await context.Users
.Where(f => f.Id == userId) .Where(f => f.Id == userId)
.ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken); .ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken);
return changedTotal > 0 return Result.FailIf(changedTotal <= 0, UpdateError.New);
? Result.Ok() }));
: Result.Fail("Failed to update user activity status.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) public async ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
{ await Result.Try((Func<ValueTask<Result<User>>>)(async () =>
try
{ {
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 return user is not null
? Result.Ok(user.Map()) ? Result.Ok(user.Map())
: Result.Fail("Failed to find specific user."); : Result.Fail(NotFoundError.New);
} }));
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -2,6 +2,6 @@ namespace PostFundManagement.Domain.Abstractions;
public interface IS3Service public interface IS3Service
{ {
Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default); ValueTask<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default); ValueTask<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default);
} }
@@ -0,0 +1,14 @@
namespace PostFundManagement.Domain.Errors;
public sealed class CreateError : Error
{
public static CreateError New { get; } = new();
public CreateError() : base(string.Empty) { }
public CreateError(string message) : base(message) { }
public static CreateError Create() => new();
public static CreateError Create(string message) => new(message);
}
@@ -0,0 +1,14 @@
namespace PostFundManagement.Domain.Errors;
public sealed class IdDecodeError : Error
{
public static IdDecodeError New { get; } = new();
public IdDecodeError() : base(string.Empty) { }
public IdDecodeError(string message) : base(message) { }
public static IdDecodeError Create() => new();
public static IdDecodeError Create(string message) => new(message);
}
@@ -0,0 +1,14 @@
namespace PostFundManagement.Domain.Errors;
public sealed class NotFoundError : Error
{
public static NotFoundError New { get; } = new();
public NotFoundError() : base(string.Empty) { }
public NotFoundError(string Message) : base(Message) { }
public static NotFoundError Create() => new();
public static NotFoundError Create(string message) => new(message);
}
@@ -0,0 +1,14 @@
namespace PostFundManagement.Domain.Errors;
public sealed class UpdateError : Error
{
public static UpdateError New { get; } = new();
public UpdateError() : base(string.Empty) { }
public UpdateError(string message) : base(message) { }
public static UpdateError Create() => new();
public static UpdateError Create(string message) => new(message);
}
@@ -0,0 +1,14 @@
namespace PostFundManagement.Domain.Errors;
public sealed class ValidationError : Error
{
public static ValidationError New { get; } = new();
public ValidationError() : base(string.Empty) { }
public ValidationError(string message) : base(message) { }
public static ValidationError Create() => new();
public static ValidationError Create(string message) => new(message);
}
@@ -2,7 +2,7 @@ namespace PostFundManagement.Domain.Extensions;
public static class Constants public static class Constants
{ {
public const string EmaiItemKey = "UserEmail"; public const string EmailtemKey = "UserEmail";
public const string UserIdItemKey = "UserId"; public const string UserIdItemKey = "UserId";