29 Commits
Author SHA1 Message Date
khwezi bd7ca9372f Merge pull request 'Added specific origin' (#28) from endpoints into main
Reviewed-on: #28
2026-09-11 14:59:34 +02:00
khwezi 0d01d91ab4 Merge pull request 'Added cors after routes' (#27) from endpoints into main
Reviewed-on: #27
2026-09-11 14:54:47 +02:00
khwezi 2c817a8818 Merge pull request 'Endpoints' (#26) from endpoints into main
Reviewed-on: #26
2026-09-11 14:35:02 +02:00
khwezi 4c3a96917b Merge pull request 'Improved results handling in service layer' (#25) from endpoints into main
Reviewed-on: #25
2026-09-11 10:33:47 +02:00
khwezi e5c3a75795 Merge pull request 'Stable run with validations' (#24) from endpoints into main
Reviewed-on: #24
2026-09-06 10:49:57 +02:00
khwezi 1bc7e65960 Merge pull request 'Added tooling' (#23) from endpoints into main
Reviewed-on: #23
2026-09-05 18:27:35 +02:00
khwezi 094126d84b Merge pull request 'Fixed endpointname uniqueness' (#22) from endpoints into main
Reviewed-on: #22
2026-09-05 13:07:41 +02:00
khwezi 2506241a25 Merge pull request 'Fixed endpoint uniqueness' (#21) from endpoints into main
Reviewed-on: #21
2026-09-05 12:58:17 +02:00
khwezi cfe3530639 Merge pull request 'Endpoints' (#20) from endpoints into main
Reviewed-on: #20
2026-09-05 12:50:04 +02:00
khwezi 985557bee4 Merge pull request 'Implemented caching on all get endpoints and service layer' (#19) from endpoints into main
Reviewed-on: #19
2026-09-05 08:30:16 +02:00
khwezi f0b8b3051a Merge pull request 'Improved getusers cache key' (#18) from endpoints into main
Reviewed-on: #18
2026-09-04 06:08:39 +02:00
khwezi 6c88b9e533 Merge pull request 'Endpoints' (#17) from endpoints into main
Reviewed-on: #17
2026-09-04 05:54:30 +02:00
khwezi 7d67408d2c Merge pull request 'Additional redis configs' (#16) from endpoints into main
Reviewed-on: #16
2026-09-04 05:39:44 +02:00
khwezi d1d58a6183 Merge pull request 'Added service level caching to IdentityService, Communication and Evidence' (#15) from endpoints into main
Reviewed-on: #15
2026-09-03 16:38:08 +02:00
khwezi 60ad154841 Merge pull request 'Implemented distributed caching' (#14) from endpoints into main
Reviewed-on: #14
2026-09-03 11:09:52 +02:00
khwezi 2c13170859 Merge pull request 'Endpoints' (#13) from endpoints into main
Reviewed-on: #13
2026-09-02 16:14:34 +02:00
khwezi d87c8aa3c9 Merge pull request 'Updated configurations' (#12) from endpoints into main
Reviewed-on: #12
2026-09-02 09:45:38 +02:00
khwezi 6af7815ad1 Merge pull request 'Removed node tooling install' (#11) from endpoints into main
Reviewed-on: #11
2026-09-02 09:30:06 +02:00
khwezi a169f6c3f6 Merge pull request 'Endpoints' (#10) from endpoints into main
Reviewed-on: #10
2026-09-02 09:28:17 +02:00
khwezi 65ea753c19 Merge pull request 'Endpoints' (#9) from endpoints into main
Reviewed-on: #9
2026-09-01 12:29:47 +02:00
khwezi e6d2663265 Merge pull request 'Endpoints' (#8) from endpoints into main
Reviewed-on: #8
2026-08-30 17:47:24 +02:00
khwezi 91b5814b60 Merge pull request 'Endpoints' (#7) from endpoints into main
Reviewed-on: #7
2026-08-30 16:27:37 +02:00
khwezi 0c4c848568 Merge pull request 'Endpoints' (#6) from endpoints into main
Reviewed-on: #6
2026-08-30 16:11:53 +02:00
khwezi fb05150ebf Merge pull request 'Endpoints' (#5) from endpoints into main
Reviewed-on: #5
2026-08-30 15:47:32 +02:00
khwezi 2f9c20020b Merge pull request 'Reogranised API and Security assets' (#4) from endpoints into main
Reviewed-on: #4
2026-08-30 15:09:32 +02:00
khwezi a771957db3 Merge pull request 'Endpoints' (#3) from endpoints into main
Reviewed-on: #3
2026-08-30 14:54:58 +02:00
khwezi 46267a2a34 Merge pull request 'Endpoints' (#2) from endpoints into main
Reviewed-on: #2
2026-08-30 14:26:08 +02:00
khwezi a7f360cc2a Merge pull request 'Security' (#1) from security into main
Reviewed-on: #1
2026-08-21 11:47:36 +02:00
hermes 67c08b40ca Refactored gitignore 2026-08-21 11:45:33 +02:00
174 changed files with 12033 additions and 35432 deletions
-21
View File
@@ -1,21 +0,0 @@
# Exclude build output and caches
**/bin/
**/obj/
**/out/
.vs/
.vscode/
# Exclude git internal files
.git/
.gitignore
# Exclude local environment/secret files
.env
*.env
*.user
*.suo
# Exclude node modules and temporary logs
**/node_modules/
**/dist/
*.log
+2 -11
View File
@@ -11,7 +11,6 @@
*.sln.docstates
*.env
*.env.json
certificates/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@@ -52,8 +51,6 @@ bld/
# Build results on 'Bin' directories
**/[Bb]in/*
dist/assets/
dist/
# Uncomment if you have tasks that rely on *.refresh files to move binaries
# (https://github.com/github/gitignore/pull/3736)
#!**/[Bb]in/*.refresh
@@ -61,7 +58,8 @@ dist/
# Visual Studio 2015/2017 cache/options directory
.vs/
.vscode/
bin/
certificates/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
@@ -349,7 +347,6 @@ _Pvt_Extensions
# Paket dependency manager
**/.paket/paket.exe
**/.paket/paket.exe
paket-files/
# FAKE - F# Make
@@ -435,9 +432,3 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp
/PostFundManagement.ConsoleUI/dist/config.json
/PostFundManagement.ConsoleUI/dist/env-config.js
/PostFundManagement.ConsoleUI/dist/assets/index-BaOzkVJi.css
/PostFundManagement.ConsoleUI/dist/assets/index-BU8qSe7k.js
/PostFundManagement.ConsoleUI/dist/favicon.svg
/PostFundManagement.ConsoleUI/dist/index.html
+28
View File
@@ -0,0 +1,28 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM node:22-bookworm-slim AS node
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG TARGETARCH
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
NUGET_XMLDOC_MODE=skip
WORKDIR /app_source
COPY . .
RUN dotnet restore "PostFundManagement.slnx"
WORKDIR "/app_source/PostFundManagement.Api"
RUN dotnet build "PostFundManagement.Api.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "PostFundManagement.Api.csproj" -c Release --no-restore -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
ENTRYPOINT ["dotnet", "PostFundManagement.Api.dll"]
-36
View File
@@ -1,36 +0,0 @@
# Stage 1: Runtime base
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
# Stage 2: Build & Restore
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy csproj files for layer caching
COPY ["PostFundManagement.Api/PostFundManagement.Api.csproj", "PostFundManagement.Api/"]
COPY ["PostFundManagement.Application/PostFundManagement.Application.csproj", "PostFundManagement.Application/"]
COPY ["PostFundManagement.Domain/PostFundManagement.Domain.csproj", "PostFundManagement.Domain/"]
COPY ["PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj", "PostFundManagement.Infrastructure/"]
# Restore packages
RUN dotnet restore "PostFundManagement.Api/PostFundManagement.Api.csproj"
# Copy full source code
COPY . .
# Build from the project folder using relative filename
WORKDIR "/src/PostFundManagement.Api"
RUN dotnet build "PostFundManagement.Api.csproj" -c Release -o /app/build
# Stage 3: Publish
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "PostFundManagement.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Stage 4: Final Runtime
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "PostFundManagement.Api.dll"]
@@ -23,7 +23,6 @@ public sealed class GetApplicantInviteEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Gets an applicant's invite")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetApplicantInviteEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Invite>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class GetApplicantInvitesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Gets an applicant's invite list")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetApplicantInvitesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Invite[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class GetInvitesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of sent invitations")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetInvitesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Invite[]>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public sealed class SendInviteEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(SendInviteEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Invite>(StatusCodes.Status200OK)
@@ -34,9 +34,9 @@ public class CreateEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
};
})
.WithDescription("Upload supporting evidence file to S3 and register metadata (M14)")
.RequireAuthorization()
.DisableAntiforgery()
.RequireAuthorization(Constants.MainScope)
.WithDescription("Upload supporting evidence file to S3 and register metadata (M14)")
.WithName(typeof(CreateEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class CreateIndicatorEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Define indicators and baselines/targets (M08)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateIndicatorEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class CreateMilestoneEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Define contractual milestones (M07)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateMilestoneRequest).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class GetEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific evidence record and assiciated file URLs")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Evidence>(StatusCodes.Status200OK)
@@ -21,7 +21,6 @@ public class GetEvidenceFilesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of all uploaded evidence files metadata")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
@@ -1,37 +0,0 @@
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.Evidence;
[ApiVersionTarget(1)]
public sealed class GetEvidenceUrlEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/evidence/{evidenceId}/securelink", async (string evidenceId, IEvidenceService service,
CancellationToken cancellationToken = default) =>
await service.GetEvidenceUrlAsync(evidenceIdHash: evidenceId, cancellationToken) switch
{
{ IsSuccess: true, Value: var evidenceUrl } => Results.Ok(evidenceUrl),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a secure download URL of the evidence file, URL expires (default: 15min)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetEvidenceUrlEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<string>(StatusCodes.Status200OK)
.Produces<string>(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -24,7 +24,6 @@ public sealed class GetIndicatorEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific indicator.")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetIndicatorEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Indicator>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetIndicatorsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Get a list of indicators")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetIndicatorsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Indicator[]>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetMilestoneEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of all uploaded evidence metadata for a specific mliestone")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetMilestoneEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetProjectEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Get a list of all uploaded evidence metadata for a specific project")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class RecordIndicatorAmountEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Record actual values for indicators (M08)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,8 +24,6 @@ public sealed class UpdateEvidenceEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Updates a specific evidence record metadata")
.RequireAuthorization(Constants.MainScope)
.DisableAntiforgery()
.WithName(typeof(UpdateEvidenceEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class ApproveDisbursementEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Approve disbursement and release payment (M06)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class CreateDisbursementEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Create a new disbursement schedule (M06)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class CreateRiskEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Add risk to risk register (M09)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public sealed class GetDisbursementEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific disbursement")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Disbursement>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetDisbursementsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of disbursements")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetDisbursementsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Disbursement[]>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public sealed class GetRiskEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific risk")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Risk>(StatusCodes.Status200OK)
@@ -21,7 +21,6 @@ public class GetRisksEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of all risks")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetRisksEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class UpdateDisbursementEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update the details of a specific disbursement")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateDisbursementEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class UpdateRiskEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update the details of a specific risk")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateRiskEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class CreateOrganisationEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create a new organization profile")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateOrganisationEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -25,7 +25,6 @@ public sealed class CreatePortfolioEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create a new organization portfolio")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreatePortfolioEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -21,7 +21,6 @@ public class DashboardEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get executive portfolio dashboard metrics and KPIs")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(DashboardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Dashboard>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class GetInstrumentsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Get all funding instruments")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Instrument[]>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class GetOrganisationEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get organization profile by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetOrganisationEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Organisation>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetOrganisationsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a paginated directory of organizations")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetOrganisationsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Organisation[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class GetPortfolioEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific portfolios")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetPortfolioEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Portfolio[]>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public sealed class GetPortfolioProgrammesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a programme's portfolios")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetPortfolioProgrammesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Programme[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class GetPortfoliosEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get all portfolios")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Portfolio[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class GetProgrammesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get all programmes")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Programme[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update an existing organization profile")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateOrganisationEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -25,7 +25,6 @@ public sealed class UpdatePortfolioEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update an existing organization portfolio")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdatePortfolioEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -25,7 +25,6 @@ public class CreateAwardEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Register an approved funding award (M01)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateAwardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -25,7 +25,6 @@ public class CreateBudgetEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create a new budget line (M05)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class CreateContractEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create a contract record from an approved award (M02)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class ExecuteContractEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Execute the contract and lock the contractual version")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class GetAwardEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get award by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetAwardEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Award>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetAwardsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of awards")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Award[]>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class GetBudgetEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get budget by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Budget>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetBudgetsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get all budgets")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Budget[]>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class GetContractEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get contract by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Contract>(StatusCodes.Status200OK)
@@ -1,37 +0,0 @@
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.Grants;
[ApiVersionTarget(1)]
public sealed class GetContractUrlEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/grants/contracts/{contractId}/securelink", async (string contractId, IGrantsService service,
CancellationToken cancellationToken = default) =>
await service.GetContractUrlAsync(contractId, cancellationToken) switch
{
{ IsSuccess: true, Value: var contractUrl} => Results.Ok(contractUrl),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a secure download URL, has expiry period (default 15min)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetContractUrlEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<string>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -22,7 +22,6 @@ public class GetContractsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get all contracts")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetContractsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Contract[]>(StatusCodes.Status200OK)
@@ -1,39 +0,0 @@
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;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public sealed class UpdateContractEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/grants/contracts", async (UpdateContractRequest request, IGrantsService service,
CancellationToken cancellationToken = default) =>
await service.UpdateContractAsync(request, 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<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Updates a specific contract")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -1,48 +0,0 @@
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.Grants;
[ApiVersionTarget(1)]
public sealed class UploadContractEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/grants/contracts/{contractId}", async (string contractId, HttpRequest request, IGrantsService service,
CancellationToken cancellationToken = default) =>
{
if (!request.HasFormContentType)
return Results.BadRequest("Request must be a multipart form.");
var form = await request.ReadFormAsync(cancellationToken);
var file = form.Files.GetFile("file");
if (file == null || file.Length == 0)
return Results.BadRequest("No file uploaded or file is empty.");
return await service.UploadContractAsync(contractId, request.HttpContext.GetUserId(), file, cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
};
})
.WithDescription("Uploads a contract file securely")
.RequireAuthorization(Constants.MainScope)
.DisableAntiforgery()
.WithName(typeof(UploadContractEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Grants)
.CacheOutput();
}
}
@@ -23,7 +23,6 @@ public class CreateActivityEndpoint : IEndpoint
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.RequireAuthorization(Constants.MainScope)
.WithDescription("Add a new activity under project (M04)")
.WithName(typeof(CreateActivityEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
@@ -25,7 +25,6 @@ public sealed class CreateProgrammeEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create programme")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateProgrammeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -25,7 +25,6 @@ public class CreateProjectEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create a new project (M04)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetActivitiesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of activities for a project")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetActivitiesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Activity[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class GetActivityEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a specific activity by it's id")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetActivityEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Domain.Models.Activity>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class GetMilestoneEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a specific milestone")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetMilestoneEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Milestone>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetMilestonesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of all milestones")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Milestone[]>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public sealed class GetProgrammeEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get specific programme")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProgrammeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Programme>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public sealed class GetProgrammeProjectsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Get a list of all projects under a programme")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProgrammeProjectsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Project[]>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public sealed class GetProgrammesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of all programmes")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName("Projects"))
.MapToApiVersion(new ApiVersion(1))
.Produces<Programme[]>(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class GetProjectEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get project by ID")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Project[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class GetProjectIndicatorsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get get a detailed list of project indicators")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectIndicatorsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Indicator[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class GetProjectMilestonesEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of milestones for a project")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Milestone[]>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class GetProjectRisksEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of risks for a project")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Risk[]>(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public class GetProjectsEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of projects")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProjectsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Project[]>(StatusCodes.Status200OK)
@@ -21,7 +21,6 @@ public sealed class UpdateMilestoneEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update project milestone details")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateMilestoneEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public sealed class UpdateProgrammeEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update existing programme")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateProgrammeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class UpdateProjectEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update project details")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -24,7 +24,6 @@ public class UpdateProjectStatusEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update project status (M04)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateProjectStatusEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -22,7 +22,6 @@ public sealed class EncodeIdEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Hashes an int64 identifier")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(EncodeIdEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<string>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class GetUserEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets the metadata of a single user.")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetUserEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<User>(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public class GetUsersEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetUsersEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<User[]>(StatusCodes.Status200OK)
@@ -30,7 +30,6 @@ public class SyncUserEndpoint : IEndpoint
};
})
.WithDescription("Trigger integration and data synchronization in the background")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(SyncUserEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -23,7 +23,6 @@ public sealed class UpdateUserStatusEndpoint : IEndpoint
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Changes the Activity status of the user in the system")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateUserStatusEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
@@ -25,8 +25,8 @@
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="IdentityModel" Version="6.2.0" />
<PackageReference Include="KubernetesClient" Version="19.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="10.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11" />
<Using Include="Microsoft.AspNetCore.Authentication" />
<Using Include="Microsoft.AspNetCore.Authentication.Cookies" />
@@ -60,8 +60,8 @@
<!-- API Documentation -->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.2" />
<Using Include="Scalar.AspNetCore" />
<Using Include="Microsoft.OpenApi" />
+12 -46
View File
@@ -1,31 +1,16 @@
using Microsoft.FeatureManagement;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Filters;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Health;
using PostFundManagement.Domain.Mediator;
using static PostFundManagement.Domain.Extensions.Constants;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFeatureManagement();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("x-api-version"),
new MediaTypeApiVersionReader("version")
);
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
builder.Services.AddEndpointsApiExplorer()
.AddApiVersioning(options => options.ApiVersionReader = new HeaderApiVersionReader());
builder.Services.AddApiVersioning().AddApiExplorer();
builder.Services.AddScoped<ActiveUserFilter>();
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
@@ -106,33 +91,23 @@ builder.Services.AddOpenApi(options =>
});
});
var allowedOrigins = builder.Configuration.GetSection("CorsOrigins").Get<string[]>() ?? [];
builder.Services.AddCors(options =>
{
options.AddPolicy(CorsPolicy, policy =>
{
policy.WithOrigins(allowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
builder.Services.AddCors(options => options.AddPolicy("AllowFrontend", policy =>
policy.WithOrigins("https://localhost:3000").AllowAnyHeader().AllowAnyMethod().AllowCredentials()));
var app = builder.Build();
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
var scheduler = await schedulerFactory.GetScheduler();
var scheduler = await schedulerFactory.GetScheduler(DefaultSchedulerName);
if (scheduler!.Status != SchedulerStatus.Running)
if (!scheduler!.IsStarted)
await scheduler.Start();
app.UseHsts();
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors(CorsPolicy);
app.UseOutputCache();
app.UseRouting();
app.UseCors("AllowFrontend");
app.UseOutputCache();
app.UseAuthentication();
app.UseAuthorization();
@@ -145,16 +120,7 @@ var versionGroups = new Dictionary<int, RouteGroupBuilder>
{
{
1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet)
.RequireCors(CorsPolicy)
.AddEndpointFilter(async (context, next) =>
{
if (HttpMethods.IsOptions(context.HttpContext.Request.Method))
return await next(context);
var filter = context.HttpContext.RequestServices.GetRequiredService<ActiveUserFilter>();
return await filter.InvokeAsync(context, next);
})
.RequireAuthorization("ApiScope").AddEndpointFilter<ActiveUserFilter>()
}
};
+15 -8
View File
@@ -1,14 +1,21 @@
{
"EnableServiceCaching": true,
"CorsOrigins": [
"https://console.ifri.khongisa.co.za",
"https://localhost:7208"
],
"GeneralS3Settings": {
"ServiceUrl": "https://s3.garage.khongisa.co.za",
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "pfm",
"CdnBaseUrl": "https://pfm.cdn.khongisa.co.za"
"BucketName": "pfm.general",
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za"
},
"ContractS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "pfm.general",
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za"
},
"EvidenceS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "pfm.general",
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za"
},
"Monitoring": {
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
@@ -10,55 +10,14 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
protected abstract string BucketName { get; }
protected abstract string CdnBaseUrl { get; }
public string GenerateCdnUrl(string fileKey) => $"{CdnBaseUrl}/{fileKey}";
public virtual async ValueTask<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default) =>
public virtual async ValueTask<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail(ValidationError.Create("Bucket name is not configured."));
if (string.IsNullOrWhiteSpace(fileKey))
return Result.Fail(ValidationError.Create("File key is required."));
var request = new GetPreSignedUrlRequest
{
BucketName = BucketName,
Key = fileKey,
Expires = DateTime.UtcNow.Add(duration),
Verb = HttpVerb.GET
};
if (!string.IsNullOrWhiteSpace(contentType))
{
request.ResponseHeaderOverrides.ContentType = contentType;
request.ResponseHeaderOverrides.ContentDisposition = "inline";
}
var presignedUrl = await Client.GetPreSignedURLAsync(request);
return Result.FailIf(string.IsNullOrWhiteSpace(presignedUrl),
CreateError.Create("Failed to create presigned URL"))
.ToResult(presignedUrl);
}));
private static string CreateFileKey(string path, string fileName, string fileHash)
{
var cleanSubPath = path.Replace('\\', '/').Trim('/');
return string.IsNullOrWhiteSpace(cleanSubPath)
? $"{fileHash.ToLower(CultureInfo.InvariantCulture)}_{fileName}"
: $"{cleanSubPath}/{fileHash.ToLower(CultureInfo.InvariantCulture)}_{fileName}";
}
public virtual async ValueTask<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail<string>(ValidationError.Create("Bucket name is not configured."));
return Result.Fail<string>(Domain.Errors.ValidationError.Create("Bucket name is not configured."));
if (string.IsNullOrWhiteSpace(CdnBaseUrl))
return Result.Fail<string>(ValidationError.Create("CDN base URL is not configured."));
return Result.Fail<string>(Domain.Errors.ValidationError.Create("CDN base URL is not configured."));
await using var stream = new MemoryStream();
@@ -72,7 +31,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
if (string.IsNullOrWhiteSpace(fileHash))
return Result.Fail<string>(CreateError.Create("Failed to compute file hash."));
var fileKey = CreateFileKey(subPath, fileName, fileHash);
var fileKey = $"{fileHash.ToLower(CultureInfo.InvariantCulture)}{Path.GetExtension(fileName)}";
var putRequest = new PutObjectRequest
{
@@ -89,7 +48,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
return response.HttpStatusCode != HttpStatusCode.OK
? Result.Fail<string>(CreateError.Create($"Failed to upload {fileName} to S3."))
: Result.Ok(fileKey);
: Result.Ok($"{CdnBaseUrl}/{fileKey}");
}));
public virtual async ValueTask<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) =>
@@ -27,6 +27,4 @@ public interface IEvidenceService
ValueTask<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default);
}
@@ -25,10 +25,4 @@ public interface IGrantsService
ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default);
}
@@ -2,7 +2,7 @@ using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Quartz;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Extensions;
namespace PostFundManagement.Domain.Extensions;
public static class Quartz
{
@@ -10,27 +10,29 @@ public static class Quartz
{
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
services.Configure<QuartzOptions>(options =>
{
options.Properties["quartz.scheduler.instanceName"] = schedulerName;
options.Properties["quartz.scheduler.instanceId"] = "AUTO";
});
services.ConfigureCommon();
services.AddQuartz(config =>
{
config.SchedulerName = schedulerName;
config.SchedulerId = "AUTO";
config.UseSimpleTypeLoader();
config.UseDefaultThreadPool(options => options.MaxConcurrency = 0);
config.UseTimeZoneConverter();
config.UsePersistentStore(storage =>
{
storage.ProvisionSchema();
storage.UsePostgres(options => options.ConnectionString = connectionString!);
storage.PerformSchemaValidation = false;
storage.UseSystemTextJsonSerializer();
storage.SetProperty("quartz.jobStore.clustered", "true");
storage.UsePostgres(options =>
{
options.ConnectionString = connectionString!;
options.TablePrefix = "quartz.qrtz_";
});
storage.UseClustering(cluster =>
{
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
@@ -46,37 +48,36 @@ public static class Quartz
{
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
services.Configure<QuartzOptions>(options =>
{
options.Properties["quartz.scheduler.instanceName"] = schedulerName;
options.Properties["quartz.scheduler.instanceId"] = "AUTO";
options.Properties["quartz.scheduler.batchTriggerAcquisitionMaxCount"] = "5";
options.Properties["quartz.jobStore.misfireThreshold"] = TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
options.Properties["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
options.Properties["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
});
services.ConfigureCommon();
services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
services.AddQuartz(config =>
{
config.SchedulerName = schedulerName;
config.SchedulerId = "AUTO";
config.InterruptJobsOnShutdown = true;
config.InterruptJobsOnShutdownWithWait = true;
config.MaxBatchSize = 5;
config.UseSimpleTypeLoader();
config.UseDefaultThreadPool(options => options.MaxConcurrency = 5);
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
config.UseTimeZoneConverter();
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture));
config.UsePersistentStore(storage =>
{
storage.ProvisionSchema();
storage.UsePostgres(options => options.ConnectionString = connectionString!);
storage.PerformSchemaValidation = false;
storage.UseSystemTextJsonSerializer();
storage.SetProperty("quartz.jobStore.clustered", "true");
storage.UsePostgres(options =>
{
options.ConnectionString = connectionString!;
options.TablePrefix = "quartz.qrtz_";
});
storage.UseClustering(cluster =>
{
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
@@ -90,7 +91,15 @@ public static class Quartz
private static IServiceCollection ConfigureCommon(this IServiceCollection services)
{
services.Configure<QuartzOptions>(options => options.Scheduling.IgnoreDuplicates = true);
services.Configure<QuartzOptions>(options =>
{
options.Scheduling.IgnoreDuplicates = true;
options.Scheduling.OverWriteExistingData = true;
options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
});
services.AddTransient<RetryJobListener>();
services.AddTransient<IJobOrchestrator, JobOrchestrator>();
@@ -25,6 +25,40 @@ public static class S3
services.AddKeyedScoped<IS3Service, GeneralS3Service>(GeneralBucketName);
}
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value))
{
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(EvidenceBucketName, (provider, client) =>
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{EvidenceS3SettingsSection}:AccessKey").Value,
configuration.GetSection($"{EvidenceS3SettingsSection}:SecretKey").Value),
new AmazonS3Config
{
ServiceURL = configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value,
AuthenticationRegion = configuration.GetSection($"{EvidenceS3SettingsSection}:Region").Value,
ForcePathStyle = true,
EndpointDiscoveryEnabled = true,
UseHttp = configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase),
}));
services.AddKeyedScoped<IS3Service, EvidenceS3Service>(EvidenceBucketName);
}
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value))
{
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(ContractBucketName, (provider, client) =>
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{ContractS3SettingsSection}:AccessKey").Value,
configuration.GetSection($"{ContractS3SettingsSection}:SecretKey").Value),
new AmazonS3Config
{
ServiceURL = configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value,
AuthenticationRegion = configuration.GetSection($"{ContractS3SettingsSection}:Region").Value,
ForcePathStyle = true,
EndpointDiscoveryEnabled = true,
UseHttp = configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase),
}));
services.AddKeyedScoped<IS3Service, ContractS3Service>(ContractBucketName);
}
return services;
}
}
@@ -1,6 +1,5 @@
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain.Security.Configuration;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security.Sdk;
using PostFundManagement.Infrastructure.Database;
@@ -98,7 +97,6 @@ public static class Security
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("role");
options.Scope.Add("email");
options.Events = new OpenIdConnectEvents
@@ -151,7 +149,7 @@ public static class Security
});
services.AddAuthorizationBuilder()
.AddPolicy(Constants.MainScope, policy =>
.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", authOptions.Audience!);
@@ -24,9 +24,9 @@
<!-- API Documentation -->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.12" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
<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" />
<Using Include="Microsoft.OpenApi" />
@@ -36,7 +36,6 @@
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Humanizer" />
<Using Include="Microsoft.FeatureManagement" />
<Using Include="System.ComponentModel.DataAnnotations" />
<Using Include="Microsoft.Extensions.Caching.Distributed" />
<Using Include="Microsoft.Extensions.Caching.Hybrid" />
@@ -4,19 +4,14 @@ using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsCacheService(CommunicationsService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, ICommunicationsService
public sealed class CommunicationsCacheService(CommunicationsService service, IDistributedCache cache) : ServiceCacheBase, ICommunicationsService
{
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetApplicantInviteAsync(applicantUserId, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getapplicantinvite-{applicantUserId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -36,9 +31,6 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getapplicantinvites-{applicantUserId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -60,9 +52,6 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetInvitesAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getinvites-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -3,7 +3,7 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Communications;
public sealed record InviteRequest([Required] long OrganisationId,
public record InviteRequest([Required] long OrganisationId,
[Required] long InvitedOrganisationId,
[Required] long AwardId,
[Required] long ContractId,
@@ -4,12 +4,10 @@ using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Evidence;
public sealed class EvidenceCacheService(EvidenceService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, 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);
@@ -23,9 +21,6 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceAsync(evidenceId, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getevidence-{evidenceId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -47,9 +42,6 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceFilesAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getevidencefiles-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -68,36 +60,9 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>());
}));
public async ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceUrlAsync(evidenceIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getevidenceurl-{evidenceIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetEvidenceUrlAsync(evidenceIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value, cancellationToken);
return result.Value;
}
return Result.Ok(cached);
}));
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIndicatorAsync(indicatorId, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getindicator-{indicatorId}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -119,9 +84,6 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIndicatorsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getindicators-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -143,9 +105,6 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetMilestoneEvidencesAsync(milestoneIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getmilestoneevidences-{milestoneIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -167,9 +126,6 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
public async ValueTask<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectEvidencesAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprojectevidences-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -10,7 +10,7 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Evidence;
public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
[FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service) : IEvidenceService
[FromKeyedServices(Constants.EvidenceBucketName)] IS3Service s3Service) : IEvidenceService
{
public async ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
@@ -22,12 +22,13 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
await using var stream = file.OpenReadStream();
var uploadResult = await s3Service.UploadFileAsync(file.FileName, "evidence", stream,
file.ContentType, cancellationToken);
var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken);
if (uploadResult.IsFailed)
return Result.Fail(uploadResult.Errors);
var documentUrl = uploadResult.Value;
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var evidence = new Domain.Entities.Evidence
@@ -40,7 +41,7 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
CreatedAt = DateTime.UtcNow,
Version = 1,
Type = type,
DocumentUrl = uploadResult.Value,
DocumentUrl = documentUrl,
Status = ApprovalStatus.Pending
};
@@ -237,31 +238,4 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(evidenceIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var evidence = await context.Evidences.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken);
if (evidence is null)
return Result.Fail<string>(NotFoundError.New);
if (string.IsNullOrWhiteSpace(evidence.DocumentUrl))
return Result.Fail<string>(ValidationError.Create("Ecidence is invalid, has no saved documentUrl"));
var secureDownloadUrlResult = await s3Service.GeneratePresignedUrlAsync(evidence.DocumentUrl,
TimeSpan.FromMinutes(15), null, cancellationToken);
return Result.OkIf(secureDownloadUrlResult.IsSuccess && !string.IsNullOrWhiteSpace(secureDownloadUrlResult.Value),
NotFoundError.Create("Failed to generate secure file url"))
.ToResult(secureDownloadUrlResult.Value);
}));
}
@@ -3,19 +3,19 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Evidence;
public sealed record UpdateEvidenceRequest([Required] long EvidenceId,
public record UpdateEvidenceRequest([Required] long EvidenceId,
[Required] int Version,
[Required] EvidenceType EvidenceType,
[Required] ApprovalStatus Status);
public sealed record RecordActualRequest([Required] decimal ActualAmount);
public record RecordActualRequest([Required] decimal ActualAmount);
public sealed record CreateMilestoneRequest([Required] long ProjectId,
public record CreateMilestoneRequest([Required] long ProjectId,
[Required] DateTime DueAt,
[Required][MaxLength(LabelLength)] string Name,
[Required] Priority Priority);
public sealed record CreateIndicatorRequest([Required] long ProjectId,
public record CreateIndicatorRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Name,
[Required] UnitOfMeasure UnitOfMeasure,
[Required] decimal BaselineAmount,
@@ -4,12 +4,10 @@ using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Execution;
public sealed class ExecutionCacheService(ExecutionService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IExecutionService
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);
@@ -23,9 +21,6 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDisbursementAsync(disbursementIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getdisbursement-{disbursementIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -47,9 +42,6 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDisbursementsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getdisbursements-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -71,9 +63,6 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRiskAsync(riskIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getrisk-{riskIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -95,9 +84,6 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRisksAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getrisks-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -3,17 +3,17 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Execution;
public sealed record UpdateDisbursementRequest([Required] long DisbursementId, long? MilestoneId, [Required] decimal Amount);
public record UpdateDisbursementRequest([Required] long DisbursementId, long? MilestoneId, [Required] decimal Amount);
public sealed record UpdateRiskRequest([Required] long RiskId,
public record UpdateRiskRequest([Required] long RiskId,
[Required] RiskLikelihood Likelihood,
[Required] RiskImpact Impact,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description);
public sealed record CreateDisbursementRequest([Required] long ProjectId, long? MilestoneId, [Required] decimal Amount);
public record CreateDisbursementRequest([Required] long ProjectId, long? MilestoneId, [Required] decimal Amount);
public sealed record CreateRiskRequest([Required] long ProjectId,
public record CreateRiskRequest([Required] long ProjectId,
[Required] RiskLikelihood Likelihood,
[Required] RiskImpact Impact,
[Required][MaxLength(LabelLength)] string Name,
@@ -4,12 +4,10 @@ using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Governance;
public sealed class GovernanceCacheService(GovernanceService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IGovernanceService
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);
@@ -20,9 +18,6 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Dashboard>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDashboardAsync(cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getdashboard").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -44,9 +39,6 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetInstrumentsAsync(programmeIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getinstruments").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -68,9 +60,6 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOrganisationAsync(organisationIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getorganisation-{organisationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -92,9 +81,6 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOrganisationsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getorganisations-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -116,9 +102,6 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPorfoliosAsync(organisationIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getportfolios-{organisationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -140,9 +123,6 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPortfolioAsync(portfolioIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getportfolio-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -164,9 +144,6 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPortfolioProgrammesAsync(portfolioIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getportfolioprogrammes-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -188,9 +165,6 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammesAsync(portfolioIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprogrammes-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -3,28 +3,28 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Governance;
public sealed record UpdatePortfolioRequest([Required] long PortfolioId,
public record UpdatePortfolioRequest([Required] long PortfolioId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description,
[Required] Guid OwnerId);
public sealed record CreatePortfolioRequest([Required][MaxLength(LabelLength)] string Name,
public record CreatePortfolioRequest([Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description,
[Required] long OrganisationId,
[Required] Guid OwnerId);
public sealed record UpdateOrganisationRequest([Required] long OrganisationId,
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 sealed record CreateOrganisationRequest([Required][MaxLength(LabelLength)] string Name,
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 sealed record Dashboard([Required] decimal PortfolioValue,
public record Dashboard([Required] decimal PortfolioValue,
[Required] int ActiveAwards,
[Required] decimal Disbursed,
[Required] double OnTrackPercentage,
@@ -4,12 +4,10 @@ using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Models;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Grants;
public sealed class GrantsCacheService(GrantsService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IGrantsService
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);
@@ -26,9 +24,6 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetAwardAsync(awardIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getaward-{awardIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -50,9 +45,6 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetAwardsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getawards-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -74,9 +66,6 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBudgetAsync(budgetIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getbudget-{budgetIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -98,9 +87,6 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBudgetsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getbudgets-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -122,9 +108,6 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractAsync(contractIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getctract-{contractIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -146,9 +129,6 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractsAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getcontracts-{pagination.Index}-{pagination.PageSize}-{pagination.Page}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
@@ -166,34 +146,4 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Contract[]>());
}));
public async ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractUrlAsync(contractIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getcontracturl-{contractIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetContractUrlAsync(contractIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value, cancellationToken);
return result.Value;
}
return Result.Ok(cached!);
}));
public async ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
await service.UpdateContractAsync(request, cancellationToken);
public async ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) =>
await service.UploadContractAsync(contractIdHash, userId, file, cancellationToken);
}
@@ -1,7 +1,6 @@
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;
@@ -9,8 +8,7 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Grants;
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
[FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service) : IGrantsService
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 () =>
@@ -203,73 +201,4 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contract = await context.Contracts.AsNoTracking().FirstOrDefaultAsync(e => e.Id == decodeResult.Value, cancellationToken);
if (contract is null)
return Result.Fail<string>(NotFoundError.New);
if (string.IsNullOrWhiteSpace(contract.SignedDocumentUrl))
return Result.Fail<string>(ValidationError.Create("Ecidence is invalid, has no saved documentUrl"));
var secureDownloadUrlResult = await s3Service.GeneratePresignedUrlAsync(contract.SignedDocumentUrl,
TimeSpan.FromMinutes(15), null, cancellationToken);
return Result.OkIf(secureDownloadUrlResult.IsSuccess && !string.IsNullOrWhiteSpace(secureDownloadUrlResult.Value),
NotFoundError.Create("Failed to generate secure file url"))
.ToResult(secureDownloadUrlResult.Value);
}));
public async ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var stream = file.OpenReadStream();
var uploadResult = await s3Service.UploadFileAsync(file.FileName, "contracts", stream,
file.ContentType, cancellationToken);
if (uploadResult.IsFailed)
return Result.Fail(uploadResult.Errors);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Contracts.Where(c => c.Id == decodeResult.Value)
.ExecuteUpdateAsync(f => f
.SetProperty(c => c.SignedDocumentUrl, uploadResult.Value), cancellationToken);
var secureDownloadUrlResult = await s3Service.GeneratePresignedUrlAsync(uploadResult.Value,
TimeSpan.FromMinutes(15), null, cancellationToken);
return Result.OkIf(changedTotal > 0 && secureDownloadUrlResult.IsSuccess && !string.IsNullOrWhiteSpace(secureDownloadUrlResult.Value),
CreateError.Create("Failed to upload contract file"));
}));
public async ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Contracts.Where(c => c.Id == request.ContractId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Status, request.Status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update the contract."));
}));
}
@@ -1,21 +1,18 @@
using PostFundManagement.Domain;
using static PostFundManagement.Domain.Extensions.Constants;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Grants;
public sealed record UpdateContractRequest([Required] long ContractId, [Required] ContractStatus Status);
public record CreateAwardRequest([Required] long OrganisationId, [Required] long ProgrammeId, [Required] decimal Amount);
public sealed record CreateAwardRequest([Required] long OrganisationId, [Required] long ProgrammeId, [Required] decimal Amount);
public sealed record CreateBudgetRequest([Required] long ProjectId,
public record CreateBudgetRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Name,
[Required] decimal Amount);
public sealed record CreateContractRequest([Required] long AwardId,
public record CreateContractRequest([Required] long AwardId,
[Required] decimal TotalValue,
[Required] DateTime EffectiveAt,
[Required] DateTime ExpiresAt);
public sealed record ExecuteContractRequest([Required] long ContractId,
public record ExecuteContractRequest([Required] long ContractId,
[Required][MaxLength(LabelLength)] string ExternalSignatureId,
[Required][MaxLength(LabelLength)] string SignedDocumentUrl);

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