diff --git a/PostFundManagement.Api/Endpoints/Communications/GetApplicantInviteEndpoint.cs b/PostFundManagement.Api/Endpoints/Communications/GetApplicantInviteEndpoint.cs index 2045f6b..41111c3 100644 --- a/PostFundManagement.Api/Endpoints/Communications/GetApplicantInviteEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Communications/GetApplicantInviteEndpoint.cs @@ -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() => Results.NotFound(), diff --git a/PostFundManagement.Api/Endpoints/Communications/GetApplicantInvitesEndpoint.cs b/PostFundManagement.Api/Endpoints/Communications/GetApplicantInvitesEndpoint.cs new file mode 100644 index 0000000..5c3c687 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Communications/GetApplicantInvitesEndpoint.cs @@ -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() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Communications) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/CreatePortfolioEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/CreatePortfolioEndpoint.cs new file mode 100644 index 0000000..de68ebc --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/CreatePortfolioEndpoint.cs @@ -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() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => 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(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/GetInstrumentsEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetInstrumentsEndpoint.cs index 8912e95..4758ca6 100644 --- a/PostFundManagement.Api/Endpoints/Governance/GetInstrumentsEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Governance/GetInstrumentsEndpoint.cs @@ -1,6 +1,7 @@ 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; @@ -13,19 +14,21 @@ public class GetInstrumentsEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/governance/instruments", async (IGovernanceService 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() => Results.Forbid(), var r => Results.InternalServerError(r.Errors.ToProblems()), }) .WithDescription("Get all funding instruments") .WithName(typeof(GetInstrumentsEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) .WithTags(EndpointTags.Governance) .CacheOutput(); } diff --git a/PostFundManagement.Api/Endpoints/Governance/GetPortfolioEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetPortfolioEndpoint.cs new file mode 100644 index 0000000..48bcd3c --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/GetPortfolioEndpoint.cs @@ -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() => Results.NotFound(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Get a specific portfolios") + .WithName(typeof(GetPortfolioEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/GetPortfolioProgrammesEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetPortfolioProgrammesEndpoint.cs new file mode 100644 index 0000000..30c193c --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/GetPortfolioProgrammesEndpoint.cs @@ -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/programmes/{portfolioId}", 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() => Results.NotFound(), + var r when r.HasError() => Results.Forbid(), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Get a programme's portfolios") + .WithName(typeof(GetPortfolioProgrammesEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/GetPortfoliosEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetPortfoliosEndpoint.cs index 2876198..d90b04b 100644 --- a/PostFundManagement.Api/Endpoints/Governance/GetPortfoliosEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Governance/GetPortfoliosEndpoint.cs @@ -1,6 +1,7 @@ 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; @@ -13,19 +14,21 @@ public class GetPortfoliosEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/governance/portfolios", async (IGovernanceService service, - CancellationToken cancellationToken = default) => - await service.GetPorfoliosAsync(cancellationToken) switch + builder.MapGet("api/governance/portfolios/{organisationId}", 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() => Results.Forbid(), var r => Results.InternalServerError(r.Errors.ToProblems()) }) .WithDescription("Get all portfolios") .WithName(typeof(GetPortfoliosEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) .WithTags(EndpointTags.Governance) .CacheOutput(); } diff --git a/PostFundManagement.Api/Endpoints/Governance/GetProgrammesEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetProgrammesEndpoint.cs index b373117..b558bb4 100644 --- a/PostFundManagement.Api/Endpoints/Governance/GetProgrammesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Governance/GetProgrammesEndpoint.cs @@ -1,6 +1,7 @@ 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; @@ -13,17 +14,19 @@ public class GetProgrammesEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/governance/programmes", async (IGovernanceService 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() => Results.Forbid(), var r => Results.InternalServerError(r.Errors.ToProblems()) }) .WithDescription("Get all programmes") .WithName(typeof(GetProgrammesEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status403Forbidden) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status500InternalServerError) .WithTags(EndpointTags.Governance) diff --git a/PostFundManagement.Api/Endpoints/Governance/UpdatePortfolioEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/UpdatePortfolioEndpoint.cs new file mode 100644 index 0000000..e041f1b --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/UpdatePortfolioEndpoint.cs @@ -0,0 +1,39 @@ +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() => Results.NotFound(), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.BadRequest(), + var r when r.HasError() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs index 1e3e77a..9a3b259 100644 --- a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs +++ b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs @@ -10,5 +10,7 @@ public interface ICommunicationsService ValueTask> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default); - ValueTask> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken = default); + ValueTask> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default); + + ValueTask> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs index 2a5a123..c1d7b61 100644 --- a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs @@ -6,19 +6,27 @@ namespace PostFundManagement.Application.Abstractions.Services; public interface IGovernanceService { - ValueTask> GetProgrammesAsync(CancellationToken cancellationToken = default); + ValueTask> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); - ValueTask> GetPorfoliosAsync(CancellationToken cancellationToken = default); + ValueTask> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default); ValueTask> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default); ValueTask> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default); - ValueTask> GetInstrumentsAsync(CancellationToken cancellationToken = default); + ValueTask> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default); ValueTask> GetDashboardAsync(CancellationToken cancellationToken = default); ValueTask CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default); ValueTask> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default); + + ValueTask CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default); + + ValueTask UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default); + + ValueTask> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); + + ValueTask> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs b/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs index a87e48b..1690679 100644 --- a/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs +++ b/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs @@ -9,16 +9,16 @@ namespace PostFundManagement.Application.Services.Communications; public sealed class CommunicationsCacheService(CommunicationsService service, IDistributedCache cache) : ServiceCacheBase, ICommunicationsService { - public async ValueTask> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken = default) => + public async ValueTask> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { - var cacheKey = HashService.ToMd5Hash($"getapplicantinvite-{userId}").Value; + var cacheKey = HashService.ToMd5Hash($"getapplicantinvite-{applicantUserId}").Value; var cached = await cache.GetStringAsync(cacheKey, cancellationToken); if (string.IsNullOrWhiteSpace(cached)) { - var result = await service.GetApplicantInviteAsync(userId, cancellationToken); + var result = await service.GetApplicantInviteAsync(applicantUserId, cancellationToken); if (result.IsFailed) return result; @@ -28,6 +28,27 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID return Result.Ok(cached!.FromJson()); })); + public async ValueTask> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(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()); + })); + public async ValueTask> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { diff --git a/PostFundManagement.Application/Services/Communications/CommunicationsService.cs b/PostFundManagement.Application/Services/Communications/CommunicationsService.cs index 237bf7f..129909e 100644 --- a/PostFundManagement.Application/Services/Communications/CommunicationsService.cs +++ b/PostFundManagement.Application/Services/Communications/CommunicationsService.cs @@ -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; @@ -58,15 +57,28 @@ public sealed class CommunicationsService(IDbContextFactory i.Map()).ToArray()); })); - public async ValueTask> GetApplicantInviteAsync(Guid userId, CancellationToken cancellationToken) => + public async ValueTask> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken) => await Result.Try((Func>>)(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(NotFoundError.New); })); + + public async ValueTask> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(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()); + })); } diff --git a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs index 6914c79..42cf471 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs @@ -12,6 +12,9 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut public async ValueTask CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.CreateOrganisationAsync(request, userId, cancellationToken); + public async ValueTask CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => + await service.CreatePortfolioAsync(request, userId, cancellationToken); + public async ValueTask> GetDashboardAsync(CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { @@ -19,7 +22,7 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut var cached = await cache.GetStringAsync(cacheKey, cancellationToken); - if(string.IsNullOrWhiteSpace(cached)) + if (string.IsNullOrWhiteSpace(cached)) { var result = await service.GetDashboardAsync(cancellationToken); @@ -33,16 +36,16 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut return Result.Ok(cached.FromJson()); })); - public async ValueTask> GetInstrumentsAsync(CancellationToken cancellationToken = default) => + public async ValueTask> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var cacheKey = HashService.ToMd5Hash($"getinstruments").Value; var cached = await cache.GetStringAsync(cacheKey, cancellationToken); - if(string.IsNullOrWhiteSpace(cached)) + if (string.IsNullOrWhiteSpace(cached)) { - var result = await service.GetInstrumentsAsync(cancellationToken); + var result = await service.GetInstrumentsAsync(programmeIdHash, cancellationToken); if (result.IsFailed) return result; @@ -96,16 +99,16 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut return Result.Ok(cached.FromJson()); })); - public async ValueTask> GetPorfoliosAsync(CancellationToken cancellationToken = default) => + public async ValueTask> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { - var cacheKey = HashService.ToMd5Hash($"getportfolios").Value; + var cacheKey = HashService.ToMd5Hash($"getportfolios-{organisationIdHash}").Value; var cached = await cache.GetStringAsync(cacheKey, cancellationToken); if (string.IsNullOrWhiteSpace(cached)) { - var result = await service.GetPorfoliosAsync(cancellationToken); + var result = await service.GetPorfoliosAsync(organisationIdHash, cancellationToken); if (result.IsFailed) return result; @@ -117,7 +120,49 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut return Result.Ok(cached.FromJson()); })); - public async ValueTask> GetProgrammesAsync(CancellationToken cancellationToken = default) => + public async ValueTask> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(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()); + })); + + public async ValueTask> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(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()); + })); + + public async ValueTask> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var cacheKey = HashService.ToMd5Hash($"getprogrammes").Value; @@ -126,7 +171,7 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut if (string.IsNullOrWhiteSpace(cached)) { - var result = await service.GetProgrammesAsync(cancellationToken); + var result = await service.GetProgrammesAsync(portfolioIdHash, cancellationToken); if (result.IsFailed) return result; @@ -140,4 +185,7 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut public async ValueTask> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) => await service.UpdateOrganisationAsync(request, cancellationToken); + + public async ValueTask UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => + await service.UpdatePortfolioAsync(request, userId, cancellationToken); } diff --git a/PostFundManagement.Application/Services/Governance/GovernanceService.cs b/PostFundManagement.Application/Services/Governance/GovernanceService.cs index db0b4c8..05bc14b 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceService.cs @@ -10,22 +10,40 @@ namespace PostFundManagement.Application.Services.Governance; public sealed class GovernanceService(IDbContextFactory contextFactory, HashService hashService) : IGovernanceService { - public async ValueTask> GetProgrammesAsync(CancellationToken cancellationToken = default) => + public async ValueTask> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(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> GetPorfoliosAsync(CancellationToken cancellationToken = default) => + public async ValueTask> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(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()); })); @@ -63,12 +81,21 @@ public sealed class GovernanceService(IDbContextFactory co : Result.Fail(NotFoundError.New); })); - public async ValueTask> GetInstrumentsAsync(CancellationToken cancellationToken = default) => + public async ValueTask> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(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()); })); @@ -136,4 +163,86 @@ public sealed class GovernanceService(IDbContextFactory co return Result.FailIf(changedTotal <= 0, UpdateError.New); })); + + public async ValueTask CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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")); + + if (await context.Portfolios.AnyAsync(p => p.Name == request.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, + OwnedBy = request.OwnerId, + Name = request.Name, + Description = request.Description + }); + + return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New); + })); + + public async ValueTask UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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) + .SetProperty(u => u.OwnedBy, request.OwnerId) + .SetProperty(u => u.Description, request.Description), cancellationToken); + + return Result.FailIf(changedTotal <= 0, UpdateError.New); + })); + + public async ValueTask> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(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> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(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 portfolio is not null + ? Result.Ok(portfolio.Map()) + : Result.Fail(NotFoundError.New); + })); } diff --git a/PostFundManagement.Application/Services/Governance/Records.cs b/PostFundManagement.Application/Services/Governance/Records.cs index 0f12a5a..b6c55a1 100644 --- a/PostFundManagement.Application/Services/Governance/Records.cs +++ b/PostFundManagement.Application/Services/Governance/Records.cs @@ -2,6 +2,10 @@ namespace PostFundManagement.Application.Services.Governance; +public record UpdatePortfolioRequest(long PortfolioId, string Name, string Description, Guid OwnerId); + +public record CreatePortfolioRequest(string Name, string Description, long OrganisationId, Guid OwnerId); + public record UpdateOrganisationRequest(long OrganisationId, string Name, string RegistrationNo, string Email, OrganisationType Type); public record CreateOrganisationRequest(string Name, string RegistrationNo, string Email, OrganisationType Type);