24 Commits
Author SHA1 Message Date
hermes ea0e4eb018 Added image name to compose file for console 2026-09-11 16:23:50 +02:00
hermes 9ebbafb840 Added more cors origins 2026-09-11 16:16:41 +02:00
hermes f3863c785d Refactored console dockerfile 2026-09-11 16:04:54 +02:00
hermes 9874eac65d Fixed dockerfile paths 2026-09-11 15:47:02 +02:00
hermes 2b8edd05e3 Supplied defaults to compose env variables 2026-09-11 15:33:15 +02:00
hermes edbae6defb Fixed compose syntax error 2026-09-11 15:30:20 +02:00
hermes b3e884336d Refactored compose file to include the ui in deployment 2026-09-11 15:28:58 +02:00
hermes 1b0ceb8ea9 Updated Console compose file to use variables 2026-09-11 15:19:04 +02:00
hermes d779e4152e Added specific origin 2026-09-11 14:59:11 +02:00
hermes 9e611cb79c Added cors after routes 2026-09-11 14:54:25 +02:00
hermes b99ebf657c Added cors to API 2026-09-11 14:34:17 +02:00
hermes 99ab5b2ce6 Added PFM Console UI project 2026-09-11 12:13:25 +02:00
hermes a15b25248d Improved results handling in service layer 2026-09-11 10:32:51 +02:00
hermes 66f720a20b Stable run with validations 2026-09-06 10:48:09 +02:00
hermes 81c111c3c3 Added tooling 2026-09-05 18:24:21 +02:00
hermes 415aa30cf7 Fixed endpointname uniqueness 2026-09-05 13:07:11 +02:00
hermes 72ea20a2de Fixed endpoint uniqueness 2026-09-05 12:57:47 +02:00
hermes 15661ce8cb Covered gvernance endpoints
Completed communication inte endpoints
2026-09-05 12:49:27 +02:00
hermes e274c33592 Migrated portfolio to be linked to the organisation 2026-09-05 10:57:10 +02:00
hermes a218b19875 Added program control endpoints 2026-09-05 10:27:52 +02:00
hermes 0a1378fe25 Implemented caching on all get endpoints and service layer 2026-09-05 08:26:15 +02:00
hermes 6719699689 Improved getusers cache key 2026-09-04 06:07:59 +02:00
hermes 8ea014e4fb Refactored cache ttl handling 2026-09-04 05:53:36 +02:00
hermes 9728459223 Removed client name definition on redis config 2026-09-04 05:46:10 +02:00
161 changed files with 22805 additions and 488 deletions
@@ -14,9 +14,9 @@ public sealed class GetApplicantInviteEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/communications/invites/applicant/{userId:Guid}", async (Guid userId, ICommunicationsService service,
builder.MapGet("api/communications/invites/applicant/{applicantUserId:Guid}", async (Guid applicantUserId, ICommunicationsService service,
HttpContext context, CancellationToken cancellationToken = default) =>
await service.GetApplicantInviteAsync(userId, cancellationToken) switch
await service.GetApplicantInviteAsync(applicantUserId, cancellationToken) switch
{
{ IsSuccess: true, Value: var invite } => Results.Ok(invite),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
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.Communications;
[ApiVersionTarget(1)]
public sealed class GetApplicantInvitesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/communications/invites/{applicantUserId:Guid}", async (Guid applicantUserId, ICommunicationsService service,
HttpContext context, CancellationToken cancellationToken = default) =>
await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken) switch
{
{ IsSuccess: true, Value: var invite } => Results.Ok(invite),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Gets an applicant's invite list")
.WithName(typeof(GetApplicantInvitesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Invite[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Communications)
.CacheOutput();
}
}
@@ -23,14 +23,15 @@ public sealed class GetIndicatorEndpoint : IEndpoint
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific indicator.")
.WithName(typeof(GetIndicatorEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Indicator>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Evidence);
.WithDescription("Get a specific indicator.")
.WithName(typeof(GetIndicatorEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Indicator>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Evidence)
.CacheOutput();
}
}
@@ -27,6 +27,7 @@ public class GetIndicatorsEndpoint : IEndpoint
.Produces<Indicator[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Evidence);
.WithTags(EndpointTags.Evidence)
.CacheOutput();
}
}
@@ -13,7 +13,7 @@ public class GetProjectEvidenceEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/evidence/milestone/{projectId}", async (string projectId, IEvidenceService service,
builder.MapGet("api/evidence/{projectId}", async (string projectId, IEvidenceService service,
CancellationToken cancellationToken = default) =>
await service.GetProjectEvidencesAsync(projectId, cancellationToken) switch
{
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class ApproveDisbursementEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/execution/disbursements/{disbursementId}/approve", async (string disbursementId,
ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
IExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
await service.ApproveDisbursementAsync(disbursementId, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
@@ -14,7 +15,7 @@ public class CreateDisbursementEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/execution/disbursements", async (CreateDisbursementRequest request,
ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
IExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
await service.CreateDisbursementAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
@@ -13,7 +14,7 @@ public class CreateRiskEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/execution/risks", async (CreateRiskRequest request, ExecutionService service,
builder.MapPost("api/execution/risks", async (CreateRiskRequest request, IExecutionService service,
HttpContext context, CancellationToken cancellationToken = default) =>
await service.CreateRiskAsync(request, context.GetUserId(), cancellationToken) switch
{
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public sealed class GetDisbursementEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/disbursements/{disbursementId}", async (string disbursementId, ExecutionService service,
builder.MapGet("api/execution/disbursements/{disbursementId}", async (string disbursementId, IExecutionService service,
CancellationToken cancellationToken = default) =>
await service.GetDisbursementAsync(disbursementIdHash: disbursementId, cancellationToken) switch
{
@@ -31,6 +31,7 @@ public sealed class GetDisbursementEndpoint : IEndpoint
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
.WithTags(EndpointTags.Execution)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetDisbursementsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/disbursements", async (ExecutionService service,
builder.MapGet("api/execution/disbursements", async (IExecutionService service,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
await service.GetDisbursementsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
{
@@ -28,6 +28,7 @@ public class GetDisbursementsEndpoint : IEndpoint
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
.WithTags(EndpointTags.Execution)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public sealed class GetRiskEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/risks/{riskId}", async (string riskId, ExecutionService service,
builder.MapGet("api/execution/risks/{riskId}", async (string riskId, IExecutionService service,
CancellationToken cancellationToken = default) =>
await service.GetRiskAsync(riskIdHash: riskId, cancellationToken) switch
{
@@ -30,6 +30,7 @@ public sealed class GetRiskEndpoint : IEndpoint
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
.WithTags(EndpointTags.Execution)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
@@ -13,7 +13,7 @@ public class GetRisksEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/risks", async (ExecutionService service,
builder.MapGet("api/execution/risks", async (IExecutionService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
await service.GetRisksAsync(Pagination.Create(page, pageSize), cancellationToken) switch
{
@@ -26,6 +26,7 @@ public class GetRisksEndpoint : IEndpoint
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
.WithTags(EndpointTags.Execution)
.CacheOutput();
}
}
@@ -1,4 +1,5 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
@@ -14,7 +15,7 @@ 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) =>
IExecutionService service, CancellationToken cancellationToken = default) =>
await service.UpdateDisbursementAsync(request, userId: context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -1,4 +1,5 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
@@ -14,7 +15,7 @@ public sealed class UpdateRiskEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/execution/risks", async (UpdateRiskRequest request,
ExecutionService service, CancellationToken cancellationToken = default) =>
IExecutionService service, CancellationToken cancellationToken = default) =>
await service.UpdateRiskAsync(request, cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
@@ -14,7 +15,7 @@ public sealed class CreateOrganisationEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/governance/organisations", async (CreateOrganisationRequest request, HttpContext context,
GovernanceService service, CancellationToken cancellationToken = default) =>
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.CreateOrganisationAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
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.Governance;
[ApiVersionTarget(1)]
public sealed class CreatePortfolioEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/governance/organisations/portfolio", async (CreatePortfolioRequest request, HttpContext context,
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.CreatePortfolioAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create a new organization portfolio")
.WithName(typeof(CreatePortfolioEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
@@ -12,7 +13,7 @@ public class DashboardEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/dashboard", async (GovernanceService service,
builder.MapGet("api/governance/dashboard", async (IGovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetDashboardAsync(cancellationToken) switch
{
@@ -25,6 +26,7 @@ public class DashboardEndpoint : IEndpoint
.Produces<Dashboard>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance);
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -1,6 +1,7 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
@@ -13,19 +14,22 @@ public class GetInstrumentsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/instruments", async (GovernanceService service,
builder.MapGet("api/governance/instruments/{programmeId}", async (string programmeId, IGovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetInstrumentsAsync(cancellationToken) switch
await service.GetInstrumentsAsync(programmeId, cancellationToken) switch
{
{ IsSuccess: true, Value: var instrumenrs } => Results.Ok(instrumenrs),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Get all funding instruments")
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Instrument[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -15,10 +15,10 @@ public class GetOrganisationEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/organisations/{organisationId}", async (string organisationId,
GovernanceService service, CancellationToken cancellationToken = default) =>
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.GetOrganisationAsync(organisationId, cancellationToken) switch
{
{ IsSuccess: true, Value: var organisation } => Results.Ok(organisationId),
{ IsSuccess: true, Value: var organisation } => Results.Ok(organisation),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
@@ -31,6 +31,7 @@ public class GetOrganisationEndpoint : IEndpoint
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance);
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetOrganisationsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/organisations", async (GovernanceService service,
builder.MapGet("api/governance/organisations", async (IGovernanceService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
await service.GetOrganisationsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
{
@@ -27,6 +27,7 @@ public class GetOrganisationsEndpoint : IEndpoint
.Produces<Organisation[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
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.Governance;
[ApiVersionTarget(1)]
public sealed class GetPortfolioEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/portfolios/{portfolioId}", async (string portfolioId, IGovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetPortfolioAsync(portfolioIdHash: portfolioId, cancellationToken) switch
{
{ IsSuccess: true, Value: var portfolio } => Results.Ok(portfolio),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific portfolios")
.WithName(typeof(GetPortfolioEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Portfolio[]>(StatusCodes.Status200OK)
.Produces<Portfolio[]>(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -0,0 +1,37 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
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.Governance;
[ApiVersionTarget(1)]
public sealed class GetPortfolioProgrammesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/portfolios/{portfolioId}/programmes", async (string portfolioId, IGovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetPortfolioProgrammesAsync(portfolioId, cancellationToken) switch
{
{ IsSuccess: true, Value: var portfolios } => Results.Ok(portfolios),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a programme's portfolios")
.WithName(typeof(GetPortfolioProgrammesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Programme[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -1,6 +1,7 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
@@ -13,19 +14,22 @@ public class GetPortfoliosEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/portfolios", async (GovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetPorfoliosAsync(cancellationToken) switch
builder.MapGet("api/governance/organisations/{organisationId}/portfolio", async (string organisationId,
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.GetPorfoliosAsync(organisationId, cancellationToken) switch
{
{ IsSuccess: true, Value: var portfolios } => Results.Ok(portfolios),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get all portfolios")
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Portfolio[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -1,6 +1,7 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
@@ -13,19 +14,22 @@ public class GetProgrammesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/programmes", async (GovernanceService service,
builder.MapGet("api/governance/programmes/{portfolioId}", async (string portfolioId, IGovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetProgrammesAsync(cancellationToken) switch
await service.GetProgrammesAsync(portfolioId, cancellationToken) switch
{
{ IsSuccess: true, Value: var programmes } => Results.Ok(programmes),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get all programmes")
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Programme[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance);
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -1,4 +1,5 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
@@ -14,7 +15,7 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/governance/organisations", async (UpdateOrganisationRequest request,
GovernanceService service, CancellationToken cancellationToken = default) =>
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.UpdateOrganisationAsync(request, cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
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.Governance;
[ApiVersionTarget(1)]
public sealed class UpdatePortfolioEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/governance/organisations/portfolio", async (UpdatePortfolioRequest request, HttpContext context,
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.UpdatePortfolioAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update an existing organization portfolio")
.WithName(typeof(UpdatePortfolioEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions;
@@ -14,7 +15,7 @@ public class CreateAwardEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/awards", async (CreateAwardRequest request, HttpContext context,
GrantsService service, CancellationToken cancellationToken = default) =>
IGrantsService service, CancellationToken cancellationToken = default) =>
await service.CreateAwardAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions;
@@ -14,7 +15,7 @@ public class CreateBudgetEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/budgets", async (CreateBudgetRequest request, HttpContext context,
GrantsService service, CancellationToken cancellationToken = default) =>
IGrantsService service, CancellationToken cancellationToken = default) =>
await service.CreateBudgetAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -23,15 +24,15 @@ public class CreateBudgetEndpoint : IEndpoint
var r when r.HasError<CreateError>() => Results.BadRequest(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create a new budget line (M05)")
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
.WithDescription("Create a new budget line (M05)")
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
}
}
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions;
@@ -14,7 +15,7 @@ public class CreateContractEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/contracts", async (CreateContractRequest request, HttpContext context,
GrantsService service, CancellationToken cancellationToken = default) =>
IGrantsService service, CancellationToken cancellationToken = default) =>
await service.CreateContractAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions;
@@ -14,7 +15,7 @@ public class ExecuteContractEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/contracts/execute", async (ExecuteContractRequest request, HttpContext context,
GrantsService service, CancellationToken cancellationToken = default) =>
IGrantsService service, CancellationToken cancellationToken = default) =>
await service.ExecuteContractAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetAwardEndpoint : IEndpoint
{
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, IGrantsService service,
CancellationToken cancellationToken = default) =>
await service.GetAwardAsync(awardId, cancellationToken) switch
{
@@ -23,14 +23,15 @@ public class GetAwardEndpoint : IEndpoint
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get award by ID")
.WithName(typeof(GetAwardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Award>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants);
.WithDescription("Get award by ID")
.WithName(typeof(GetAwardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Award>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
@@ -14,19 +14,20 @@ public class GetAwardsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/awards", async (GrantsService service,
builder.MapGet("api/grants/awards", async (IGrantsService service,
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
await service.GetAwardsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
{
{ IsSuccess: true, Value: var awards } => Results.Ok(awards),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of awards")
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Award[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
.WithDescription("Get a list of awards")
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Award[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetBudgetEndpoint : IEndpoint
{
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, IGrantsService service,
CancellationToken cancellationToken = default) =>
await service.GetBudgetAsync(budgetId, cancellationToken) switch
{
@@ -23,14 +23,15 @@ public class GetBudgetEndpoint : IEndpoint
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get budget by ID")
.WithName(typeof(GetBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Budget>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants);
.WithDescription("Get budget by ID")
.WithName(typeof(GetBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Budget>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
@@ -14,19 +14,20 @@ public class GetBudgetsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/budgets", async (GrantsService service,
builder.MapGet("api/grants/budgets", async (IGrantsService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
await service.GetBudgetsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
{
{ IsSuccess: true, Value: var budgets } => Results.Ok(budgets),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get all budgets")
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Budget[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
.WithDescription("Get all budgets")
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Budget[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetContractEndpoint : IEndpoint
{
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, IGrantsService service,
CancellationToken cancellationToken = default) =>
await service.GetContractAsync(contractId, cancellationToken) switch
{
@@ -23,14 +23,15 @@ public class GetContractEndpoint : IEndpoint
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get contract by ID")
.WithName(typeof(GetContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Contract>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants);
.WithDescription("Get contract by ID")
.WithName(typeof(GetContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Contract>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetContractsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/contracts", async (GrantsService service,
builder.MapGet("api/grants/contracts", async (IGrantsService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
await service.GetContractsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
{
@@ -27,6 +27,7 @@ public class GetContractsEndpoint : IEndpoint
.Produces<Contract[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Grants);
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
@@ -14,7 +15,7 @@ public class CreateActivityEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/activities", async (CreateActivityRequest request,
HttpContext context, ProjectService service, CancellationToken cancellationToken = default) =>
HttpContext context, IProjectService service, CancellationToken cancellationToken = default) =>
await service.CreateActivityAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } result => Results.Ok(),
@@ -0,0 +1,37 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
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.Projects;
[ApiVersionTarget(1)]
public sealed class CreateProgrammeEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/programme", async (CreateProgrammeRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.CreateProgrammeAsync(request, userId: context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<CreateError>() => Results.BadRequest(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create programme")
.WithName(typeof(CreateProgrammeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
@@ -14,7 +15,7 @@ public class CreateProjectEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects", async (CreateProjectRequest request, HttpContext context,
ProjectService service, CancellationToken cancellationToken = default) =>
IProjectService service, CancellationToken cancellationToken = default) =>
await service.CreateProjectAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } result => Results.Ok(),
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetActivitiesEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId}/activities", async (string projectId,
ProjectService service, CancellationToken cancellationToken = default) =>
IProjectService service, CancellationToken cancellationToken = default) =>
await service.GetActivitiesAsync(projectId, cancellationToken) switch
{
{ IsSuccess: true, Value: var activities } => Results.Ok(activities),
@@ -28,6 +28,7 @@ public class GetActivitiesEndpoint : IEndpoint
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetActivityEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/activities/{activityId}", async (string activityId,
ProjectService service, CancellationToken cancellationToken = default) =>
IProjectService service, CancellationToken cancellationToken = default) =>
await service.GetActivityAsync(activityId, cancellationToken) switch
{
{ IsSuccess: true, Value: var activity } => Results.Ok(activity),
@@ -30,6 +30,7 @@ public class GetActivityEndpoint : IEndpoint
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetMilestoneEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/milestones/{milestoneId}", async (string milestoneId, ProjectService service,
builder.MapGet("api/projects/milestones/{milestoneId}", async (string milestoneId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetMilestoneAsync(milestoneId, cancellationToken) switch
{
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
@@ -14,20 +14,20 @@ public class GetMilestonesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/milestones/{page:int}/{pageSize:int}", async (ProjectService service,
builder.MapGet("api/projects/milestones/{page:int}/{pageSize:int}", async (IProjectService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
await service.GetMilestonesAsync(Pagination.Create(page, pageSize), cancellationToken) switch
{
{ IsSuccess: true, Value: var milestones } => Results.Ok(milestones),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.RequireAuthorization()
.WithDescription("Get a list of all milestones")
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Milestone[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
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 sealed class GetProgrammeEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/programme/{programmeId}", async (string programmeId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetProgrammeAsync(programmeId, cancellationToken) switch
{
{ IsSuccess: true, Value: var programme } => Results.Ok(programme),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get specific programme")
.WithName(typeof(GetProgrammeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Programme>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,37 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
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 sealed class GetProgrammeProjectsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{programmeId}/programme", async (string programmeId,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.GetProgrammeProjectsAsync(programmeId, cancellationToken) switch
{
{ IsSuccess: true, Value: var projects } => Results.Ok(projects),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Get a list of all projects under a programme")
.WithName(typeof(GetProgrammeProjectsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Project[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
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 sealed class GetProgrammesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/programme/{page:int}/{pageSize:int}", async (IProjectService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
await service.GetProgrammesAsync(Pagination.Create(page, pageSize), cancellationToken) switch
{
{ IsSuccess: true, Value: var programmes } => Results.Ok(programmes),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of all programmes")
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName("Projects"))
.MapToApiVersion(new ApiVersion(1))
.Produces<Programme[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetProjectEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId}", async (string projectId, ProjectService service,
builder.MapGet("api/projects/{projectId}", async (string projectId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetProjectAsync(projectId, cancellationToken) switch
{
@@ -31,6 +31,7 @@ public class GetProjectEndpoint : IEndpoint
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetProjectIndicatorsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/indicators/project/{projectId}", async (string projectId, ProjectService service,
builder.MapGet("api/projects/indicators/project/{projectId}", async (string projectId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetProjectIndicatorsAsync(projectIdHash: projectId, cancellationToken) switch
{
@@ -30,6 +30,7 @@ public class GetProjectIndicatorsEndpoint : IEndpoint
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -15,7 +15,7 @@ public class GetProjectMilestonesEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId}/milestones", async (string projectId,
ProjectService service, CancellationToken cancellationToken = default) =>
IProjectService service, CancellationToken cancellationToken = default) =>
await service.GetProjectMilestonesAsync(projectId, cancellationToken) switch
{
{ IsSuccess: true, Value: var milestones } => Results.Ok(milestones),
@@ -30,6 +30,7 @@ public class GetProjectMilestonesEndpoint : IEndpoint
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
@@ -15,7 +15,7 @@ public class GetProjectRisksEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId}/risks", async (string projectId,
ProjectService service, CancellationToken cancellationToken = default) =>
IProjectService service, CancellationToken cancellationToken = default) =>
await service.GetProjectRisksAsync(projectId, cancellationToken) switch
{
{ IsSuccess: true, Value: var risks } => Results.Ok(risks),
@@ -30,6 +30,7 @@ public class GetProjectRisksEndpoint : IEndpoint
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -1,5 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
@@ -14,7 +14,7 @@ public class GetProjectsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects", async (ProjectService service,
builder.MapGet("api/projects", async (IProjectService service,
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
await service.GetProjectsAsync(Pagination.Create(page, pageSize), cancellationToken) switch
{
@@ -27,6 +27,7 @@ public class GetProjectsEndpoint : IEndpoint
.Produces<Project[]>(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -1,4 +1,5 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
@@ -12,7 +13,7 @@ public sealed class UpdateMilestoneEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects/milestone", async (UpdateMilestoneRequest request, ProjectService service,
builder.MapPut("api/projects/milestone", async (UpdateMilestoneRequest request, IProjectService service,
HttpContext context, CancellationToken cancellationToken = default) =>
await service.UpdateMilestoneAsync(request, context.GetUserId(), cancellationToken) switch
{
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
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.Projects;
[ApiVersionTarget(1)]
public sealed class UpdateProgrammeEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects/programme", async (UpdateProgrammeRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.UpdateProgrammeAsync(request, userId: context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update existing programme")
.WithName(typeof(UpdateProgrammeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
@@ -1,4 +1,5 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
@@ -14,20 +15,20 @@ public sealed class UpdateProjectEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects", async (UpdateProjectRequest request,
ProjectService service, HttpContext context, CancellationToken cancellationToken = default) =>
IProjectService service, HttpContext context, CancellationToken cancellationToken = default) =>
await service.UpdateProjectAsync(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 project details")
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
.WithDescription("Update project details")
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -1,3 +1,4 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
@@ -14,7 +15,7 @@ public class UpdateProjectStatusEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects/status", async (UpdateProjectStatusRequest request, HttpContext context,
ProjectService service, CancellationToken cancellationToken = default) =>
IProjectService service, CancellationToken cancellationToken = default) =>
await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
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.Tools;
[ApiVersionTarget(1)]
public sealed class EncodeIdEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/tools/hashid/{id64:long}", async (long id64, IToolService service,
CancellationToken cancellationToken) =>
await service.EncodeLong(id64, cancellationToken) switch
{
{ IsSuccess: true, Value: var hash } => Results.Ok(hash),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Hashes an int64 identifier")
.WithName(typeof(EncodeIdEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<string>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Tools)
.CacheOutput();
}
}
@@ -10,6 +10,14 @@
<PackageLicenseFile>..\LICENSE</PackageLicenseFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;AV0029;AV0030;</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;AV0029;AV0030;</NoWarn>
</PropertyGroup>
<!-- Security (IODC)-->
<ItemGroup>
<PackageReference Include="IdentityModel.AspNetCore" Version="4.3.0" />
+12 -2
View File
@@ -42,6 +42,9 @@ builder.Services.AddHealthChecks()
IEnumerable<string> identityScopes = ["openid", "profile", "roles", "email", "address"];
builder.Services.AddValidation()
.AddEndpointValidation();
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
@@ -88,6 +91,12 @@ builder.Services.AddOpenApi(options =>
});
});
builder.Services.AddCors(options => options.AddPolicy("AllowFrontend", policy =>
policy.WithOrigins("https://localhost:3000", "https://localhost:8076", "https://console.ifri.khongisa.co.za")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()));
var app = builder.Build();
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
@@ -100,6 +109,7 @@ app.UseHsts();
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("AllowFrontend");
app.UseOutputCache();
app.UseAuthentication();
app.UseAuthorization();
@@ -111,7 +121,7 @@ ApiVersionSet versionSet = app.NewApiVersionSet("v1")
var versionGroups = new Dictionary<int, RouteGroupBuilder>
{
{
{
1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet)
.RequireAuthorization("ApiScope").AddEndpointFilter<ActiveUserFilter>()
}
@@ -140,7 +150,7 @@ foreach (var description in app.DescribeApiVersions().OrderByDescending(o => o.A
app.MapScalarApiReference($"/openapi/{description.GroupName}", (options, context) =>
{
options.AddServer(new ScalarServer($"https://{context.Request.Host}"));
options.WithOpenApiRoutePattern($"/openapi/{description.GroupName}.json");
options.WithOpenApiRoutePattern($"/openapi/{description.GroupName}.json");
options.WithTheme(ScalarTheme.DeepSpace);
options.Agent = new ScalarAgentOptions { Disabled = true };
options.AddPreferredSecuritySchemes("OAuth2")
@@ -0,0 +1,9 @@
namespace PostFundManagement.Application.Abstractions;
public abstract class ServiceCacheBase
{
protected readonly DistributedCacheEntryOptions cacheOptions = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
};
}
@@ -10,5 +10,7 @@ public interface ICommunicationsService
ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,26 @@
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
public interface IExecutionService
{
ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,32 @@
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
public interface IGovernanceService
{
ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default);
ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default);
ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,28 @@
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
public interface IGrantsService
{
ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,46 @@
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
public interface IProjectService
{
ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,6 @@
namespace PostFundManagement.Application.Abstractions.Services;
public interface IToolService
{
ValueTask<Result<string>> EncodeLong(long id, CancellationToken cancellationToken);
}
@@ -6,6 +6,8 @@ namespace PostFundManagement.Application.Extensions;
public static class Api
{
public static IServiceCollection AddEndpointValidation(this IServiceCollection services) => services.AddValidation();
public static IServiceCollection AddEndpointAndServiceCaching(this IServiceCollection services, IConfiguration configuration)
{
services.AddStackExchangeRedisOutputCache(options =>
@@ -1,56 +1,12 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Application.Services.Users;
using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Application.Extensions;
namespace PostFundManagement.Application.Extensions;
public static class General
{
public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<ICommunicationsService, CommunicationsService>();
services.Decorate<ICommunicationsService, CommunicationsCacheService>();
public static string Clean(this string input) => Patterns.SpaceRegex().Replace(input, string.Empty);
services.AddScoped<IEvidenceService, EvidenceService>();
services.Decorate<IEvidenceService, EvidenceCacheService>();
public static string ToJson<TValue>(this TValue obj) where TValue : class => JsonSerializer.Serialize(obj);
services.AddScoped<IUserService, UserService>();
services.Decorate<IUserService, UserCacheService>();
services.AddScoped<IIdentityService, IdentityService>();
services.Decorate<IIdentityService, IdentityCacheService>();
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = configuration.GetConnectionString("Redis");
options.ConfigurationOptions!.ClientName = "PFM-API";
options.InstanceName = "PFM-API_";
});
services.AddHybridCache(options => options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(2),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
});
return services;
}
public static IServiceCollection AddSharedServices(this IServiceCollection services)
{
var serviceType = typeof(IService);
var sharedImplementations = typeof(IApplication).Assembly.GetTypes()
.Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
foreach (var sharedImplementation in sharedImplementations) services.AddScoped(sharedImplementation);
return services;
}
public static TValue FromJson<TValue>(this string json) where TValue : class => JsonSerializer.Deserialize<TValue>(json)!;
public static Guid GetUserId(this HttpContext context) => (Guid)context.Items[Domain.Extensions.Constants.UserIdItemKey]!;
@@ -78,6 +34,6 @@ public static class General
{
Detail = string.Join(',', errors.Select(e => e.Message)),
Status = StatusCodes.Status400BadRequest,
Title = errors[0]?.Message,
Title = "Error",
};
}
@@ -0,0 +1,16 @@
namespace PostFundManagement.Application.Extensions;
public partial class Patterns
{
[GeneratedRegex(@"\s+")]
public static partial Regex SpaceRegex();
[GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
public static partial Regex HexHashRegex { get; }
[GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
public static partial Regex Md5Regex { get; }
[GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
public static partial Regex Sha256Regex { get; }
}
@@ -0,0 +1,73 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Application.Services.Tools;
using PostFundManagement.Application.Services.Users;
using PostFundManagement.Domain.Abstractions;
namespace PostFundManagement.Application.Extensions;
public static class Services
{
public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<ICommunicationsService, CommunicationsService>();
services.Decorate<ICommunicationsService, CommunicationsCacheService>();
services.AddScoped<IEvidenceService, EvidenceService>();
services.Decorate<IEvidenceService, EvidenceCacheService>();
services.AddScoped<IUserService, UserService>();
services.Decorate<IUserService, UserCacheService>();
services.AddScoped<IIdentityService, IdentityService>();
services.Decorate<IIdentityService, IdentityCacheService>();
services.AddScoped<IExecutionService, ExecutionService>();
services.Decorate<IExecutionService, ExecutionCacheService>();
services.AddScoped<IGovernanceService, GovernanceService>();
services.Decorate<IGovernanceService, GovernanceCacheService>();
services.AddScoped<IGrantsService, GrantsService>();
services.Decorate<IGrantsService, GrantsCacheService>();
services.AddScoped<IProjectService, ProjectService>();
services.Decorate<IProjectService, ProjectCacheService>();
services.AddScoped<IToolService, ToolService>();
services.Decorate<IToolService, ToolCacheService>();
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = configuration.GetConnectionString("Redis");
options.InstanceName = "PFM-API_";
});
services.AddHybridCache(options => options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(2),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
});
return services;
}
public static IServiceCollection AddSharedServices(this IServiceCollection services)
{
var serviceType = typeof(IService);
var sharedImplementations = typeof(IApplication).Assembly.GetTypes()
.Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
foreach (var sharedImplementation in sharedImplementations) services.AddScoped(sharedImplementation);
return services;
}
}
@@ -9,6 +9,14 @@
<PackageLicenseFile>..\LICENSE</PackageLicenseFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;AV0029;AV0030;</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;AV0029;AV0030;</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\PostFundManagement.Domain\PostFundManagement.Domain.csproj" />
<ProjectReference Include="..\PostFundManagement.Infrastructure\PostFundManagement.Infrastructure.csproj" />
@@ -17,6 +25,7 @@
<!-- API Documentation -->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
<PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.11" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.2" />
<Using Include="Scalar.AspNetCore" />
@@ -26,6 +35,9 @@
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Humanizer" />
<Using Include="System.ComponentModel.DataAnnotations" />
<Using Include="Microsoft.Extensions.Caching.Distributed" />
<Using Include="Microsoft.Extensions.Caching.Hybrid" />
<Using Include="System.IdentityModel.Tokens.Jwt" />
<Using Include="Asp.Versioning" />
@@ -1,21 +1,74 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsCacheService(CommunicationsService service, HybridCache cache) : ICommunicationsService
public sealed class CommunicationsCacheService(CommunicationsService service, IDistributedCache cache) : ServiceCacheBase, ICommunicationsService
{
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken = default) =>
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
await cache.GetOrCreateAsync(HashService.ToMd5Hash($"getapplicantinvite-{userId}").Value,
async cancel => await service.GetApplicantInviteAsync(userId, cancellationToken))));
{
var cacheKey = HashService.ToMd5Hash($"getapplicantinvite-{applicantUserId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetApplicantInviteAsync(applicantUserId, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
}
return Result.Ok(cached!.FromJson<Invite>());
}));
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getapplicantinvites-{applicantUserId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Invite[]>());
}));
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
await cache.GetOrCreateAsync(HashService.ToMd5Hash($"getinvites-{pagination.Page}-{pagination.PageSize}").Value,
async cancel => await service.GetInvitesAsync(pagination, cancellationToken))));
{
var cacheKey = HashService.ToMd5Hash($"getinvites-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetInvitesAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return Result.Ok(result.Value);
}
return Result.Ok(cached.FromJson<Invite[]>());
}));
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.SendInviteAsync(request, userId, cancellationToken);
@@ -1,6 +1,5 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
@@ -39,9 +38,8 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
CreatedAt = DateTime.UtcNow
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(invite.Map())
: Result.Fail<Invite>(CreateError.New);
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New)
.ToResult(invite.Map());
}));
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
@@ -58,15 +56,27 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
return Result.Ok(invites.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken) =>
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
{
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 == applicantUserId, cancellationToken);
return invite is not null
? Result.Ok(invite.Map())
: Result.Fail<Invite>(NotFoundError.New);
return Result.FailIf(invite is null, NotFoundError.New)
.ToResult(invite!.Map());
}));
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var invites = await context.Invites.AsNoTracking()
.Where(i => i.Recipient == applicantUserId)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(invites.Select(i => i.Map()).ToArray());
}));
}
@@ -1,5 +1,12 @@
using PostFundManagement.Domain;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Communications;
public record InviteRequest(long OrganisationId, long InvitedOrganisationId, long AwardId, long ContractId, Guid Recipient, string Message, NotificationPlatform Platform);
public record InviteRequest([Required] long OrganisationId,
[Required] long InvitedOrganisationId,
[Required] long AwardId,
[Required] long ContractId,
[Required] Guid Recipient,
[Required][MaxLength(LabelLength)] string Message,
[Required] NotificationPlatform Platform);
@@ -1,11 +1,13 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Services.Evidence;
public sealed class EvidenceCacheService(EvidenceService service, HybridCache cache) : IEvidenceService
public sealed class EvidenceCacheService(EvidenceService service, IDistributedCache cache) : ServiceCacheBase, IEvidenceService
{
public async ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
await service.CreateEvidenceAsync(userId, form, file, cancellationToken);
@@ -18,33 +20,129 @@ public sealed class EvidenceCacheService(EvidenceService service, HybridCache ca
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence>>>)(async () =>
await cache.GetOrCreateAsync(HashService.ToMd5Hash($"getevidence-{evidenceId}").Value,
async cancel => await service.GetEvidenceAsync(evidenceId, cancellationToken))));
{
var cacheKey = HashService.ToMd5Hash($"getevidence-{evidenceId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetEvidenceAsync(evidenceId, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Domain.Models.Evidence>());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
await cache.GetOrCreateAsync(HashService.ToMd5Hash($"getevidencefiles-{pagination.Page}-{pagination.PageSize}").Value,
async cancel => await service.GetEvidenceFilesAsync(pagination, cancellationToken))));
{
var cacheKey = HashService.ToMd5Hash($"getevidencefiles-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetEvidenceFilesAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>());
}));
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () =>
await cache.GetOrCreateAsync(HashService.ToMd5Hash($"getindicator-{indicatorId}").Value,
async cancel => await service.GetIndicatorAsync(indicatorId, cancellationToken))));
{
var cacheKey = HashService.ToMd5Hash($"getindicator-{indicatorId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetIndicatorAsync(indicatorId, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Indicator>());
}));
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
await cache.GetOrCreateAsync(HashService.ToMd5Hash($"getindicators-{pagination.Page}-{pagination.PageSize}").Value,
async cancel => await service.GetIndicatorsAsync(pagination, cancellationToken))));
{
var cacheKey = HashService.ToMd5Hash($"getindicators-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetIndicatorsAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Indicator[]>());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
await cache.GetOrCreateAsync(HashService.ToMd5Hash($"getmilestoneevidences-{milestoneIdHash}").Value,
async cancel => await service.GetMilestoneEvidencesAsync(milestoneIdHash, cancellationToken))));
{
var cacheKey = HashService.ToMd5Hash($"getmilestoneevidences-{milestoneIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetMilestoneEvidencesAsync(milestoneIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
await cache.GetOrCreateAsync(HashService.ToMd5Hash($"getprojectevidences-{projectIdHash}").Value,
async cancel => await service.GetProjectEvidencesAsync(projectIdHash, cancellationToken))));
{
var cacheKey = HashService.ToMd5Hash($"getprojectevidences-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProjectEvidencesAsync(projectIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>());
}));
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) =>
await service.RecordIndicatorActualAmountAsync(indicatorIdHash, amount, userId, cancellationToken);
@@ -47,7 +47,7 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
context.Evidences.Add(evidence);
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) =>
@@ -66,7 +66,7 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.SetProperty(f => f.ActualAmount, amount)
.SetProperty(f => f.CreatedBy, userId), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
@@ -102,6 +102,9 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken))
return Result.Fail(NotFoundError.New);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Result.Fail(NotFoundError.New);
@@ -116,7 +119,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
CreatedAt = DateTime.UtcNow
});
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0,
CreateError.Create("No record could be created."));
}));
public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) =>
@@ -139,7 +143,7 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
CreatedAt = DateTime.UtcNow
});
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) =>
@@ -155,9 +159,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
var indicator = await context.Indicators.AsNoTracking().FirstOrDefaultAsync(i => i.Id == decodeResult.Value, cancellationToken);
return indicator is not null
? Result.Ok(indicator.Map())
: Result.Fail(NotFoundError.New);
return Result.FailIf(indicator is null, NotFoundError.New)
.ToResult(indicator!.Map());
}));
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
@@ -173,9 +176,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
var evidence = await context.Evidences.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken);
return evidence is not null
? Result.Ok(evidence.Map())
: Result.Fail<Domain.Models.Evidence>(NotFoundError.New);
return Result.FailIf(evidence is null, NotFoundError.New)
.ToResult(evidence!.Map());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
@@ -234,6 +236,6 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.SetProperty(f => f.UpdatedBy, userId)
.SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
}
@@ -1,11 +1,22 @@
using PostFundManagement.Domain;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Evidence;
public record UpdateEvidenceRequest(long EvidenceId, int Version, EvidenceType EvidenceType, ApprovalStatus Status);
public record UpdateEvidenceRequest([Required] long EvidenceId,
[Required] int Version,
[Required] EvidenceType EvidenceType,
[Required] ApprovalStatus Status);
public record RecordActualRequest(decimal ActualAmount);
public record RecordActualRequest([Required] decimal ActualAmount);
public record CreateMilestoneRequest(long ProjectId, DateTime DueAt, string Name, Priority Priority);
public record CreateMilestoneRequest([Required] long ProjectId,
[Required] DateTime DueAt,
[Required][MaxLength(LabelLength)] string Name,
[Required] Priority Priority);
public record CreateIndicatorRequest(long ProjectId, string Name, UnitOfMeasure UnitOfMeasure, decimal BaselineAmount, decimal TargetAmount);
public record CreateIndicatorRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Name,
[Required] UnitOfMeasure UnitOfMeasure,
[Required] decimal BaselineAmount,
[Required] decimal TargetAmount);
@@ -0,0 +1,110 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Services.Execution;
public sealed class ExecutionCacheService(ExecutionService service, IDistributedCache cache) : ServiceCacheBase, IExecutionService
{
public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) =>
await service.ApproveDisbursementAsync(disbursementIdHash, userId, cancellationToken);
public async ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateDisbursementAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateRiskAsync(request, userId, cancellationToken);
public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getdisbursement-{disbursementIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetDisbursementAsync(disbursementIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Disbursement>());
}));
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getdisbursements-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetDisbursementsAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Disbursement[]>());
}));
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getrisk-{riskIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetRiskAsync(riskIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Risk>());
}));
public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getrisks-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetRisksAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Risk[]>());
}));
public async ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateDisbursementAsync(request, userId, cancellationToken);
public async ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default) =>
await service.UpdateRiskAsync(request, cancellationToken);
}
@@ -1,6 +1,6 @@
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
@@ -8,7 +8,7 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Execution;
public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IExecutionService
{
public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
@@ -57,7 +57,7 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
CreatedAt = DateTime.UtcNow
});
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
@@ -78,7 +78,7 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
CreatedAt = DateTime.UtcNow
});
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) =>
@@ -98,7 +98,7 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.SetProperty(f => f.UpdatedBy, userId)
.SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken) =>
@@ -115,12 +115,11 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
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);
return Result.FailIf(disbursement is null, NotFoundError.New)
.ToResult(disbursement!.Map());
}));
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken) =>
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(riskIdHash);
@@ -134,12 +133,11 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
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);
return Result.FailIf(risk is null, NotFoundError.New)
.ToResult(risk!.Map());
}));
public async ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken) =>
public async ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -152,10 +150,10 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.SetProperty(u => u.Likelihood, request.Likelihood)
.SetProperty(u => u.Impact, request.Impact), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken) =>
public async ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -168,6 +166,6 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.SetProperty(d => d.UpdatedAt, DateTime.UtcNow)
.SetProperty(d => d.UpdatedBy, userId), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
}
@@ -1,11 +1,20 @@
using PostFundManagement.Domain;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Execution;
public record UpdateDisbursementRequest(long DisbursementId, long? MilestoneId, decimal Amount);
public record UpdateDisbursementRequest([Required] long DisbursementId, long? MilestoneId, [Required] decimal Amount);
public record UpdateRiskRequest(long RiskId, RiskLikelihood Likelihood, RiskImpact Impact, string Name, string Description);
public record UpdateRiskRequest([Required] long RiskId,
[Required] RiskLikelihood Likelihood,
[Required] RiskImpact Impact,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description);
public record CreateDisbursementRequest(long ProjectId, long? MilestoneId, decimal Amount);
public record CreateDisbursementRequest([Required] long ProjectId, long? MilestoneId, [Required] decimal Amount);
public record CreateRiskRequest(long ProjectId, RiskLikelihood Likelihood, RiskImpact Impact, string Name, string Description);
public record CreateRiskRequest([Required] long ProjectId,
[Required] RiskLikelihood Likelihood,
[Required] RiskImpact Impact,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description);
@@ -0,0 +1,191 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Services.Governance;
public sealed class GovernanceCacheService(GovernanceService service, IDistributedCache cache) : ServiceCacheBase, IGovernanceService
{
public async ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateOrganisationAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreatePortfolioAsync(request, userId, cancellationToken);
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Dashboard>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getdashboard").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetDashboardAsync(cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Dashboard>());
}));
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getinstruments").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetInstrumentsAsync(programmeIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Instrument[]>());
}));
public async ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getorganisation-{organisationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetOrganisationAsync(organisationIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Organisation>());
}));
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getorganisations-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetOrganisationsAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Organisation[]>());
}));
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getportfolios-{organisationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetPorfoliosAsync(organisationIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Portfolio[]>());
}));
public async ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getportfolio-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetPortfolioAsync(portfolioIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Portfolio>());
}));
public async ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getportfolioprogrammes-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetPortfolioProgrammesAsync(portfolioIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Programme[]>());
}));
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getprogrammes-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProgrammesAsync(portfolioIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Programme[]>());
}));
public async ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) =>
await service.UpdateOrganisationAsync(request, cancellationToken);
public async ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdatePortfolioAsync(request, userId, cancellationToken);
}
@@ -1,31 +1,50 @@
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database;
using PostFundManagement.Application.Extensions;
namespace PostFundManagement.Application.Services.Governance;
public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IGovernanceService
{
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(CancellationToken cancellationToken = default) =>
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var programmes = await context.Programmes.AsNoTracking().ToListAsync(cancellationToken);
var programmes = await context.Programmes.AsNoTracking()
.Where(p => p.PortfolioId == decodeResult.Value)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(programmes.Select(p => p.Map()).ToArray());
}));
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(CancellationToken cancellationToken = default) =>
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(organisationIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var portfolios = await context.Portfolios.AsNoTracking().ToListAsync(cancellationToken);
var portfolios = await context.Portfolios.AsNoTracking()
.Where(f => f.Id == decodeResult.Value)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(portfolios.Select(p => p.Map()).ToArray());
}));
@@ -58,17 +77,25 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
var organisation = await context.Organisations.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken);
return organisation is not null
? Result.Ok(organisation.Map())
: Result.Fail(NotFoundError.New);
return Result.FailIf(organisation is null, NotFoundError.New)
.ToResult(organisation!.Map());
}));
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(CancellationToken cancellationToken = default) =>
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(programmeIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var instruments = await context.Instruments.AsNoTracking().ToListAsync(cancellationToken);
var instruments = await context.Instruments.AsNoTracking()
.Where(i => i.ProgrammeId == decodeResult.Value)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(instruments.Select(i => i.Map()).ToArray());
}));
@@ -104,21 +131,23 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Organisations.AnyAsync(o => o.RegistrationNo == request.RegistrationNo, cancellationToken))
var registrationNo = request.RegistrationNo.Clean();
if (await context.Organisations.AnyAsync(o => o.RegistrationNo == registrationNo, cancellationToken))
return Result.Fail(CreateError.Create("Organisation with the same registration number already exists"));
context.Organisations.Add(new Domain.Entities.Organisation
{
Name = request.Name,
RegistrationNo = request.RegistrationNo,
Email = request.Email,
Name = request.Name.Humanize(LetterCasing.Title).Trim(),
RegistrationNo = registrationNo,
Email = request.Email.ToLower(CultureInfo.InvariantCulture).Clean(),
Type = request.Type,
Status = ActivityStatus.Active,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
});
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) =>
@@ -129,11 +158,95 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
var changedTotal = await context.Organisations
.Where(f => f.Id == request.OrganisationId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Name, request.Name)
.SetProperty(u => u.RegistrationNo, request.RegistrationNo)
.SetProperty(u => u.Email, request.Email)
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.RegistrationNo, request.RegistrationNo.Clean())
.SetProperty(u => u.Email, request.Email.ToLower(CultureInfo.InvariantCulture).Clean())
.SetProperty(u => u.Type, request.Type), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Users.AnyAsync(u => u.Id == request.OwnerId, cancellationToken))
return Result.Fail(NotFoundError.Create("Owner user not found"));
if (!await context.Organisations.AnyAsync(u => u.Id == request.OrganisationId, cancellationToken))
return Result.Fail(NotFoundError.Create("Organisation not found"));
var name = request.Name.Humanize(LetterCasing.Title).Trim();
if (await context.Portfolios.AnyAsync(p => p.Name == name && p.OrganisationId == request.OrganisationId, cancellationToken))
return Result.Fail(ValidationError.Create("A portfolio with the same name exists in this organisation"));
context.Portfolios.Add(new Domain.Entities.Portfolio
{
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
OrganisationId = request.OrganisationId,
OwnedBy = request.OwnerId,
Name = name,
Description = request.Description.Humanize(LetterCasing.Sentence).Trim()
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Users.AnyAsync(u => u.Id == request.OwnerId, cancellationToken))
return Result.Fail(NotFoundError.Create("Owner user not found"));
var changedTotal = await context.Portfolios
.Where(p => p.Id == request.PortfolioId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.OwnedBy, request.OwnerId)
.SetProperty(u => u.Description, request.Description.Humanize(LetterCasing.Sentence).Trim()), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("Nothing was saved, no change detected."));
}));
public async ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var programmes = await context.Programmes.AsNoTracking()
.Where(f => f.PortfolioId == decodeResult.Value)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(programmes.Select(p => p.Map()).ToArray());
}));
public async ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var portfolio = await context.Portfolios.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(portfolio is null, NotFoundError.New)
.ToResult(portfolio!.Map());
}));
}
@@ -1,9 +1,32 @@
using PostFundManagement.Domain;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Governance;
public record UpdateOrganisationRequest(long OrganisationId, string Name, string RegistrationNo, string Email, OrganisationType Type);
public record UpdatePortfolioRequest([Required] long PortfolioId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description,
[Required] Guid OwnerId);
public record CreateOrganisationRequest(string Name, string RegistrationNo, string Email, OrganisationType Type);
public record CreatePortfolioRequest([Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description,
[Required] long OrganisationId,
[Required] Guid OwnerId);
public record Dashboard(decimal PortfolioValue, int ActiveAwards, decimal Disbursed, double OnTrackPercentage, int AtRiskCount, int BeneficiariesReached);
public record UpdateOrganisationRequest([Required] long OrganisationId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string RegistrationNo,
[Required][EmailAddress][MaxLength(LabelLength)] string Email,
[Required] OrganisationType Type);
public record CreateOrganisationRequest([Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string RegistrationNo,
[Required][EmailAddress][MaxLength(LabelLength)] string Email,
[Required] OrganisationType Type = OrganisationType.PrivateLimitedCompany);
public record Dashboard([Required] decimal PortfolioValue,
[Required] int ActiveAwards,
[Required] decimal Disbursed,
[Required] double OnTrackPercentage,
[Required] int AtRiskCount,
[Required] int BeneficiariesReached);
@@ -0,0 +1,149 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Services.Grants;
public sealed class GrantsCacheService(GrantsService service, IDistributedCache cache) : ServiceCacheBase, IGrantsService
{
public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateAwardAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateBudgetAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateContractAsync(request, userId, cancellationToken);
public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.ExecuteContractAsync(request, userId, cancellationToken);
public async ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getaward-{awardIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetAwardAsync(awardIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Award>());
}));
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getawards-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetAwardsAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Award[]>());
}));
public async ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getbudget-{budgetIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetBudgetAsync(budgetIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Budget>());
}));
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getbudgets-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetBudgetsAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Budget[]>());
}));
public async ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getctract-{contractIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetContractAsync(contractIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Contract>());
}));
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getcontracts-{pagination.Index}-{pagination.PageSize}-{pagination.Page}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetContractsAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Contract[]>());
}));
}
@@ -1,6 +1,6 @@
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
@@ -8,7 +8,7 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Grants;
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IGrantsService
{
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () =>
@@ -38,9 +38,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
var contract = await context.Contracts.AsNoTracking()
.FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken);
return contract is not null
? Result.Ok(contract.Map())
: Result.Fail<Contract>(NotFoundError.New);
return Result.FailIf(contract is null, NotFoundError.New)
.ToResult(contract!.Map());
}));
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
@@ -71,9 +70,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
var budget = await context.Budgets.AsNoTracking()
.FirstOrDefaultAsync(b => b.Id == decodeResult.Value, cancellationToken);
return budget is not null
? Result.Ok(budget.Map())
: Result.Fail<Budget>(NotFoundError.New);
return Result.FailIf(budget is null, NotFoundError.New)
.ToResult(budget!.Map());
}));
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
@@ -103,9 +101,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
var award = await context.Awards.AsNoTracking().FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken);
return award is not null
? Result.Ok(award.Map())
: Result.Fail<Award>(NotFoundError.New);
return Result.FailIf(award is null, NotFoundError.New)
.ToResult(award!.Map());
}));
public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
@@ -121,7 +118,7 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.SetProperty(u => u.CreatedBy, userId)
.SetProperty(u => u.Status, ContractStatus.Active), cancellationToken);
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, UpdateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, UpdateError.New);
}));
public async ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
@@ -148,7 +145,7 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
context.Contracts.Add(contract);
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
@@ -173,7 +170,7 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
context.Budgets.Add(budget);
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
@@ -202,6 +199,6 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
context.Awards.Add(award);
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
}
@@ -1,9 +1,18 @@
namespace PostFundManagement.Application.Services.Grants;
using static PostFundManagement.Domain.Extensions.Constants;
public record CreateAwardRequest(long OrganisationId, long ProgrammeId, decimal Amount);
namespace PostFundManagement.Application.Services.Grants;
public record CreateBudgetRequest(long ProjectId, string Name, decimal Amount);
public record CreateAwardRequest([Required] long OrganisationId, [Required] long ProgrammeId, [Required] decimal Amount);
public record CreateContractRequest(long AwardId, decimal TotalValue, DateTime EffectiveAt, DateTime ExpiresAt);
public record CreateBudgetRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Name,
[Required] decimal Amount);
public record ExecuteContractRequest(long ContractId, string ExternalSignatureId, string SignedDocumentUrl);
public record CreateContractRequest([Required] long AwardId,
[Required] decimal TotalValue,
[Required] DateTime EffectiveAt,
[Required] DateTime ExpiresAt);
public record ExecuteContractRequest([Required] long ContractId,
[Required][MaxLength(LabelLength)] string ExternalSignatureId,
[Required][MaxLength(LabelLength)] string SignedDocumentUrl);
@@ -0,0 +1,284 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Services.Projects;
public sealed class ProjectCacheService(ProjectService service, IDistributedCache cache) : ServiceCacheBase, IProjectService
{
public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateActivityAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateProgrammeAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateProjectAsync(request, userId, cancellationToken);
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getactivities-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetActivitiesAsync(projectIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Domain.Models.Activity[]>());
}));
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getactivity-{activityIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetActivityAsync(activityIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Domain.Models.Activity>());
}));
public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getmilestone-{milestoneIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetMilestoneAsync(milestoneIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Milestone>());
}));
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getmilestones-{pagination.Index}-{pagination.PageSize}-{pagination.Page}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetMilestonesAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Milestone[]>());
}));
public async ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getprogramme-{programmeIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProgrammeAsync(programmeIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached!.FromJson<Programme>());
}));
public async ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getprogrammeprojects-{programmeIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProgrammeProjectsAsync(programmeIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Project[]>());
}));
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getprogrammes-{pagination.Index}-{pagination.PageSize}-{pagination.Page}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProgrammesAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Programme[]>());
}));
public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getproject-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProjectAsync(projectIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Project>());
}));
public async ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getprojectindicators-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProjectIndicatorsAsync(projectIdHash, cancellationToken);
if(result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Indicator[]>());
}));
public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getprojectmilestones-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProjectMilestonesAsync(projectIdHash, cancellationToken);
if(result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Milestone[]>());
}));
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getprojectrisks-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProjectRisksAsync(projectIdHash, cancellationToken);
if(result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Risk[]>());
}));
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getprojects-{pagination.Index}-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProjectsAsync(pagination, cancellationToken);
if(result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Project[]>());
}));
public async ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateMilestoneAsync(request, userId, cancellationToken);
public async ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateProgrammeAsync(request, userId, cancellationToken);
public async ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateProjectAsync(request, userId, cancellationToken);
public async ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateProjectStatusAsync(request, userId, cancellationToken);
}
@@ -1,6 +1,6 @@
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
@@ -8,32 +8,24 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Projects;
public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IProjectService
{
public async ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var project = await context.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, cancellationToken);
if (project == null)
if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken))
return Result.Fail(NotFoundError.New);
project.Status = request.Status;
project.UpdatedBy = userId;
project.UpdatedAt = DateTime.UtcNow;
context.Projects.Update(project);
var changedTotal = await context.Projects
.Where(p => p.Id != request.ProjectId)
.Where(f => f.Id == request.ProjectId)
.ExecuteUpdateAsync(f => f
.SetProperty(p => p.Status, request.Status)
.SetProperty(p => p.UpdatedBy, userId)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
return Result.FailIf(changedTotal == 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled."));
}));
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
@@ -63,9 +55,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
var project = await context.Projects.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken);
return project is not null
? Result.Ok(project.Map())
: Result.Fail<Project>(NotFoundError.Create());
return Result.FailIf(project is null, NotFoundError.New)
.ToResult(project!.Map());
}));
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
@@ -91,17 +82,21 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
{
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))
var name = request.Name.Humanize(LetterCasing.Title).Trim();
if (await context.Projects.AnyAsync(p => p.Name == name && p.Sector == request.Sector && p.ProjectType == request.ProjectType, cancellationToken))
return Result.Fail(ValidationError.Create("A similarly named project exists of the same project type and sector"));
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
return Result.Fail(NotFoundError.New);
var description = request.Description.Humanize(LetterCasing.Sentence).Trim();
context.Projects.Add(new Domain.Entities.Project
{
ProgrammeId = request.ProgrammeId,
Name = request.Name,
Description = request.Description,
Name = name,
Description = description,
Sector = request.Sector,
ProjectType = request.ProjectType,
Status = ApprovalStatus.Pending,
@@ -109,7 +104,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
CreatedAt = DateTime.UtcNow
});
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0,
CreateError.Create("No changes detected, update cancelled."));
}));
public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) =>
@@ -128,9 +124,9 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
{
ProjectId = request.ProjectId,
MilestoneId = request.MilestoneId,
Category = request.Category,
Category = request.Category.Humanize(LetterCasing.Title).Trim(),
Amount = request.Amount,
Title = request.Title,
Title = request.Title.Humanize(LetterCasing.Title).Trim(),
Description = request.Description,
PerformedAt = request.PerformedAt,
Status = ApprovalStatus.Pending,
@@ -138,7 +134,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
CreatedAt = DateTime.UtcNow
});
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0,
CreateError.Create("No record was created."));
}));
public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
@@ -179,7 +176,7 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(evidences.Select(e => e.Map()).ToArray());
}));
public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken) =>
public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash);
@@ -192,9 +189,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
var milestone = await context.Milestones.AsNoTracking()
.FirstOrDefaultAsync(m => m.Id == decodeResult.Value, cancellationToken);
return milestone is not null
? Result.Ok(milestone.Map())
: Result.Fail<Milestone>(NotFoundError.Create());
return Result.FailIf(milestone is null, NotFoundError.New)
.ToResult(milestone!.Map());
}));
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
@@ -211,32 +207,35 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(milestones.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken) =>
public async ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken))
return Result.Fail(NotFoundError.New);
var changedTotal = await context.Milestones.Where(m => m.Id == request.MilestoneId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Name, request.Name)
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.DueAt, request.DueAt)
.SetProperty(u => u.Priority, request.Priority)
.SetProperty(u => u.Status, request.Status)
.SetProperty(u => u.UpdatedBy, userId)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled."));
}));
public async ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken) =>
public async ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Projects.Where(p => p.Id == request.ProjectId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Name, request.Name)
.SetProperty(u => u.Description, request.Description)
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.Description, request.Description.Humanize(LetterCasing.Sentence).Trim())
.SetProperty(u => u.StartedAt, request.StartedAt)
.SetProperty(u => u.EndedAt, request.EndedAt)
.SetProperty(u => u.UpdatedBy, userId)
@@ -244,10 +243,10 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.SetProperty(u => u.Sector, request.Sector)
.SetProperty(u => u.ProjectType, request.ProjectType), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled."));
}));
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken) =>
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(activityIdHash);
@@ -261,9 +260,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
var activity = await context.Activities.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken);
return activity is not null
? Result.Ok(activity.Map())
: Result.Fail<Domain.Models.Activity>(NotFoundError.New);
return Result.FailIf(activity is null, NotFoundError.New)
.ToResult(activity!.Map());
}));
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
@@ -284,4 +282,100 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(risks.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var name = request.Name.Humanize(LetterCasing.Title).Trim();
if (!await context.Portfolios.AnyAsync(p => p.Id == request.PortfolioId, cancellationToken))
return Result.Fail(ValidationError.Create("Portfolio does not exist."));
if (await context.Programmes.AnyAsync(p => p.Name == name, cancellationToken))
return Result.Fail(ValidationError.Create("Programme of the same name exists"));
context.Programmes.Add(new Domain.Entities.Programme
{
PortfolioId = request.PortfolioId,
OwnedBy = request.OwnerId,
Name = name,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow,
Status = ApprovalStatus.Pending
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken))
return Result.Fail(NotFoundError.New);
var changedTotal = await context.Programmes.Where(p => p.Id == request.ProgrammeId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.OwnedBy, request.OwnerId)
.SetProperty(u => u.Status, request.Status)
.SetProperty(u => u.UpdatedBy, userId)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled."));
}));
public async ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(programmeIdHash);
if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var programme = await context.Programmes.FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(programme is null, NotFoundError.New)
.ToResult(programme!.Map());
}));
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var programmes = await context.Programmes.AsNoTracking()
.OrderByDescending(m => m.CreatedAt)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(programmes.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(programmeIdHash);
if (decodeResult.IsFailed) return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Programmes.AnyAsync(p => p.Id == decodeResult.Value, cancellationToken))
return Result.Fail(NotFoundError.New);
var projects = await context.Projects.AsNoTracking()
.Where(p => p.ProgrammeId == decodeResult.Value)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
return Result.Ok(projects.Select(i => i.Map()).ToArray());
}));
}
@@ -1,13 +1,40 @@
using PostFundManagement.Domain;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Projects;
public record UpdateProjectRequest(long ProjectId, string Name, string Description, DateTime StartedAt, DateTime? EndedAt, IndustrySector Sector, ProjectType ProjectType);
public record UpdateProgrammeRequest([Required] long ProgrammeId,
[Required][MaxLength(LabelLength)] string Name,
[Required] Guid OwnerId,
[Required] ApprovalStatus Status);
public record UpdateMilestoneRequest(long MilestoneId, DateTime DueAt, string Name, Priority Priority, ApprovalStatus Status);
public record CreateProgrammeRequest(long PortfolioId, string Name, Guid OwnerId);
public record UpdateProjectStatusRequest(long ProjectId, ApprovalStatus Status);
public record UpdateProjectRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description,
DateTime? StartedAt, DateTime? EndedAt,
[Required] IndustrySector Sector,
[Required] ProjectType ProjectType);
public record CreateProjectRequest(long ProgrammeId, string Name, string Description, IndustrySector Sector, ProjectType ProjectType);
public record UpdateMilestoneRequest([Required] long MilestoneId,
[Required] DateTime DueAt,
[Required][MaxLength(LabelLength)] string Name,
[Required] Priority Priority,
[Required] ApprovalStatus Status);
public record CreateActivityRequest(long ProjectId, long? MilestoneId, string Category, decimal Amount, string Title, string Description, DateTime? PerformedAt);
public record UpdateProjectStatusRequest([Required] long ProjectId, [Required] ApprovalStatus Status);
public record CreateProjectRequest([Required] long ProgrammeId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description,
[Required] IndustrySector Sector,
[Required] ProjectType ProjectType);
public record CreateActivityRequest([Required] long ProjectId,
[Required] long? MilestoneId,
[Required][MaxLength(LabelLength)] string Category,
[Required] decimal Amount,
[Required][MaxLength(LabelLength)] string Title,
[Required][MaxLength(TextLength)] string Description,
DateTime? PerformedAt);
@@ -33,9 +33,9 @@ public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
{
var retrieval = await storage.GetAsync<string>(key);
return retrieval.Success && !string.IsNullOrWhiteSpace(retrieval.Value)
? Result.Ok(retrieval.Value)
: Result.Fail(NotFoundError.Create($"Could not find object by key {key}"));
return Result.FailIf(!retrieval.Success && string.IsNullOrWhiteSpace(retrieval.Value),
NotFoundError.Create($"Could not find object by key {key}"))
.ToResult(retrieval.Value!);
}));
public async ValueTask<Result<TValue>> GetAsync<TValue>(string key) where TValue : class =>
@@ -43,8 +43,8 @@ public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
{
var retrieval = await storage.GetAsync<TValue>(key);
return retrieval.Success && retrieval.Value is not null
? Result.Ok(retrieval.Value)
: Result.Fail(NotFoundError.Create($"Could not find object by key {key}"));
return Result.FailIf(!retrieval.Success && retrieval.Value is null,
NotFoundError.Create($"Could not find object by key {key}"))
.ToResult(retrieval.Value!);
}));
}
@@ -1,24 +1,16 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Application.Extensions;
namespace PostFundManagement.Application.Services.Shared;
public sealed partial class HashService(IHashids hasher) : IService
{
[GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
private static partial Regex HexHashRegex { get; }
[GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
private static partial Regex Md5Regex { get; }
[GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
private static partial Regex Sha256Regex { get; }
public static bool IsMd5Hash(string? value) =>
!string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value);
!string.IsNullOrWhiteSpace(value) && Patterns.Md5Regex.IsMatch(value);
public static bool IsSha256Hash(string? value) =>
!string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value);
!string.IsNullOrWhiteSpace(value) && Patterns.Sha256Regex.IsMatch(value);
public static string? StringToSha256Hash(string? input) =>
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
@@ -39,17 +31,18 @@ public sealed partial class HashService(IHashids hasher) : IService
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
}
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input)
? Result.Fail<string>(ValidationError.Create("Input must be a valid hexadecimal string."))
: Result.Ok(hasher.EncodeHex(input));
public Result<string> HashEncodeHex(string input) =>
Result.FailIf(string.IsNullOrWhiteSpace(input) || !Patterns.HexHashRegex.IsMatch(input),
ValidationError.Create("Input must be a valid hexadecimal string."))
.ToResult(hasher.EncodeHex(input));
public Result<string> HashEncodeIntId(int id) => id < 0
? Result.Fail<string>(ValidationError.Create("Id cannot be negative."))
: Result.Ok(hasher.Encode(id));
public Result<string> HashEncodeIntId(int id) =>
Result.FailIf(id < 0, ValidationError.Create("Id cannot be negative."))
.ToResult(hasher.Encode(id));
public Result<string> HashEncodeLongId(long id) => id < 0
? Result.Fail<string>(ValidationError.Create("Id cannot be negative."))
: Result.Ok(hasher.EncodeLong(id));
public Result<string> HashEncodeLongId(long id) =>
Result.FailIf(id < 0, ValidationError.Create("Id cannot be negative."))
.ToResult(hasher.EncodeLong(id));
public Result<int> DecodeIntIdHash(string hash)
{
@@ -58,9 +51,8 @@ public sealed partial class HashService(IHashids hasher) : IService
int[] decoded = hasher.Decode(hash);
return decoded.Length == 1
? Result.Ok(decoded[0]) :
Result.Fail<int>(ValidationError.Create("Invalid or modified Int hash token."));
return Result.FailIf(decoded.Length <= 0, ValidationError.Create("Invalid or modified Int hash token."))
.ToResult(decoded[0]);
}
public Result<long> DecodeLongIdHash(string hash)
@@ -70,8 +62,8 @@ public sealed partial class HashService(IHashids hasher) : IService
long[] decoded = hasher.DecodeLong(hash);
return decoded.Length == 1 ? Result.Ok(decoded[0]) :
Result.Fail<long>(ValidationError.Create("Invalid or modified Long hash token."));
return Result.FailIf(decoded.Length <= 0, ValidationError.Create("Invalid or modified Long hash token."))
.ToResult(decoded[0]);
}
public async Task<Result<string>> DecodeHexHash(string hex) =>
@@ -79,8 +71,7 @@ public sealed partial class HashService(IHashids hasher) : IService
{
string decoded = hasher.DecodeHex(hex);
return string.IsNullOrEmpty(decoded)
? Result.Fail<string>(ValidationError.Create("Invalid or corrupted hex hash."))
: Result.Ok(decoded);
return Result.FailIf(string.IsNullOrEmpty(decoded), ValidationError.Create("Invalid or corrupted hex hash."))
.ToResult(decoded);
}));
}
@@ -1,31 +1,10 @@
using Microsoft.Extensions.Caching.Distributed;
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Domain.Errors;
namespace PostFundManagement.Application.Services.Shared;
public sealed class IdentityCacheService(IdentityService service, IDistributedCache cache) : IIdentityService
public sealed class IdentityCacheService(IdentityService service) : ServiceCacheBase, IIdentityService
{
public async ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"userisauthorised-{userId}").Value;
var cached = await cache.GetStringAsync(cacheKey);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.UserIsAuthorisedAsync(userId, cancellationToken); ;
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, "true", cancellationToken);
return Result.Ok();
}
return cached.Equals("true", StringComparison.OrdinalIgnoreCase)
? Result.Ok()
: Result.Fail(NotFoundError.New);
}));
await service.UserIsAuthorisedAsync(userId, cancellationToken);
}
@@ -15,6 +15,6 @@ public sealed class IdentityService(IDbContextFactory<ApplicationDbContext> cont
var userExistsAndActive = await context.Users
.AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken);
return Result.FailIf(!userExistsAndActive, NotFoundError.Create());
return Result.OkIf(userExistsAndActive, NotFoundError.Create());
}));
}
@@ -29,16 +29,16 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
{
var tokenResponse = JsonSerializer.Deserialize<Domain.Security.Models.TokenResponse>(contentRaw);
return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken)
? Result.Ok(tokenResponse)
: Result.Fail<Domain.Security.Models.TokenResponse>(NotFoundError.Create("Authentication succeeded, but no access token was found in the response payload."));
return Result.FailIf(string.IsNullOrWhiteSpace(tokenResponse?.AccessToken),
NotFoundError.Create("Authentication succeeded, but no access token was found in the response payload."))
.ToResult(tokenResponse!);
}
return await Result.Try((Func<ValueTask<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
{
var errorResult = JsonSerializer.Deserialize<TokenErrorResponse>(contentRaw);
return Result.FailIf(errorResult != null,
return Result.OkIf(errorResult is not null,
CreateError.Create($"{errorResult!.Error}: {errorResult.ErrorDescription}"));
}));
}));
@@ -0,0 +1,29 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
namespace PostFundManagement.Application.Services.Tools;
public sealed class ToolCacheService(ToolService service, IDistributedCache cache) : ServiceCacheBase, IToolService
{
public async ValueTask<Result<string>> EncodeLong(long id, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"encodelonghash-{id}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = (await service.EncodeLong(id).ConfigureAwait(false));
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value, cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached);
}));
}
@@ -0,0 +1,10 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
namespace PostFundManagement.Application.Services.Tools;
public sealed class ToolService(HashService service) : IToolService
{
public ValueTask<Result<string>> EncodeLong(long id, CancellationToken cancellationToken = default) =>
ValueTask.FromResult(service.HashEncodeLongId(id));
}
@@ -1,12 +1,13 @@
using Microsoft.Extensions.Caching.Distributed;
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Services.Users;
public sealed class UserCacheService(UserService service, IDistributedCache cache) : IUserService
public sealed class UserCacheService(UserService service, IDistributedCache cache) : ServiceCacheBase, IUserService
{
public async ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<User>>>)(async () =>
@@ -21,20 +22,19 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach
if (userResult.IsFailed) return userResult;
await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(userResult.Value));
await cache.SetStringAsync(cacheKey, userResult.Value.ToJson(), cacheOptions, cancellationToken);
return userResult;
}
var restored = JsonSerializer.Deserialize<User>(cached)!;
return Result.Ok(restored);
return Result.Ok(cached.FromJson<User>());
}));
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<User[]>>>)(async () =>
{
var cacheKey = HashService.ToMd5Hash($"getusers-{pagination.Page}-{pagination.PageSize}-{request.Email}-{request.Status}").Value;
var cacheKey = HashService.ToMd5Hash($"getusers-{pagination.Page}-{pagination.PageSize}-" +
$"{request.Email}-{request.Status}-{request.SortBy}-{request.SortDescending}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -44,14 +44,13 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach
if (usersResult.IsFailed) return usersResult;
await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(usersResult.Value));
await cache.SetStringAsync(cacheKey, usersResult.Value.ToJson(),
cacheOptions, cancellationToken);
return usersResult;
}
var restored = JsonSerializer.Deserialize<User[]>(cached)!;
return Result.Ok(restored);
return Result.Ok(cached.FromJson<User[]>());
}));
public async ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default) =>
@@ -32,7 +32,7 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
context.Users.Update(user);
}
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
@@ -74,7 +74,7 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
.Where(f => f.Id == userId)
.ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken);
return Result.FailIf(changedTotal <= 0, UpdateError.New);
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
@@ -85,8 +85,7 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
var user = await context.Users.AsNoTracking()
.FirstOrDefaultAsync(f => f.Id == userId, cancellationToken);
return user is not null
? Result.Ok(user.Map())
: Result.Fail(NotFoundError.New);
return Result.FailIf(user is null, NotFoundError.New)
.ToResult(user!.Map());
}));
}
+18
View File
@@ -0,0 +1,18 @@
# GEMINI_API_KEY: Required for Gemini AI API calls.
# AI Studio automatically injects this at runtime from user secrets.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
# APP_URL: The URL where this applet is hosted.
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"
# OIDC SecuritySettings
VITE_OIDC_AUTHORITY="https://sts.security.khongisa.co.za"
VITE_OIDC_AUDIENCE="pfm-api-dev"
VITE_OIDC_CLIENT_ID="pfm-scalar-dev"
VITE_OIDC_CLIENT_SECRET="secret_71f88570a46eb22eb5270b47f69e3332c84d8db85d008f0276cb7431c8872d27"
# PFM Backend API Base URL
VITE_API_BASE_URL="https://api.ifri.khongisa.co.za/"
+8
View File
@@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example
+41
View File
@@ -0,0 +1,41 @@
# Stage 1: Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Enable corepack to use Yarn
RUN corepack enable
# Copy dependency definitions from the subdirectory
COPY PostFundManagement.ConsoleUI/package*.json PostFundManagement.ConsoleUI/yarn.lock* ./
# Install dependencies via Yarn
RUN yarn install --frozen-lockfile || yarn install
# Copy application source from the subdirectory
COPY PostFundManagement.ConsoleUI/ ./
# Build Vite application for production
RUN yarn build
# Stage 2: Production Nginx Server
FROM nginx:alpine
WORKDIR /usr/share/nginx/html
# Copy nginx configuration from stage 1 build directory
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
# Copy compiled static assets from build stage
COPY --from=builder /app/dist ./
# Copy entrypoint script for dynamic runtime environment variables
COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Expose port 3000
EXPOSE 3000
# Set entrypoint and default command
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

Some files were not shown because too many files have changed in this diff Show More