From 15a8d8f44932b1d7024fd34aa91f7f576092e87a Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 26 Sep 2026 12:03:47 +0200 Subject: [PATCH] Fixed portfolio retrieval --- .../GetOrganisationPortfoliosEndpoint.cs | 36 +++++++++++++++++++ .../Governance/GetPortfoliosEndpoint.cs | 13 ++++--- .../Services/IGovernanceService.cs | 5 ++- .../Governance/GovernanceCacheService.cs | 32 ++++++++++++++--- .../Services/Governance/GovernanceService.cs | 18 ++++++++-- 5 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 PostFundManagement.Api/Endpoints/Governance/GetOrganisationPortfoliosEndpoint.cs diff --git a/PostFundManagement.Api/Endpoints/Governance/GetOrganisationPortfoliosEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetOrganisationPortfoliosEndpoint.cs new file mode 100644 index 0000000..a2071a0 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/GetOrganisationPortfoliosEndpoint.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 class GetOrganisationPortfoliosEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/governance/organisations/{organisationId}/portfolio", async (string organisationId, + IGovernanceService service, CancellationToken cancellationToken = default) => + await service.GetOrganisationPorfoliosAsync(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 belonging to a specific organisation") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetOrganisationPortfoliosEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .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 18047dc..dd8b94e 100644 --- a/PostFundManagement.Api/Endpoints/Governance/GetPortfoliosEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Governance/GetPortfoliosEndpoint.cs @@ -1,7 +1,7 @@ -using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; -using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Security; @@ -10,16 +10,15 @@ using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; namespace PostFundManagement.Api.Endpoints.Governance; [ApiVersionTarget(1)] -public class GetPortfoliosEndpoint : IEndpoint +public sealed class GetPortfoliosEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/governance/organisations/{organisationId}/portfolio", async (string organisationId, - IGovernanceService service, CancellationToken cancellationToken = default) => - await service.GetPorfoliosAsync(organisationId, cancellationToken) switch + builder.MapPost("api/governance/organisations/portfolios", async (Pagination pagination, IGovernanceService service, + CancellationToken cancellationToken = default) => + await service.GetPorfoliosAsync(pagination, 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") diff --git a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs index 6ae9d81..90c8147 100644 --- a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs @@ -19,7 +19,10 @@ public interface IGovernanceService Task> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); [Log] - Task> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default); + Task> GetPorfoliosAsync(Pagination pagination, CancellationToken cancellationToken = default); + + [Log] + Task> GetOrganisationPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default); [Log] Task> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default); diff --git a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs index e9bdd22..6b8170f 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs @@ -122,19 +122,19 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut return Result.Ok(cached.FromJson()); })); - public async Task> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) => + public async Task> GetOrganisationPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) - return await service.GetPorfoliosAsync(organisationIdHash, cancellationToken); + return await service.GetOrganisationPorfoliosAsync(organisationIdHash, cancellationToken); - var cacheKey = HashService.ToMd5Hash($"getportfolios-{organisationIdHash}").Value; + var cacheKey = HashService.ToMd5Hash($"getorganisationportfolios-{organisationIdHash}").Value; var cached = await cache.GetStringAsync(cacheKey, cancellationToken); if (string.IsNullOrWhiteSpace(cached)) { - var result = await service.GetPorfoliosAsync(organisationIdHash, cancellationToken); + var result = await service.GetOrganisationPorfoliosAsync(organisationIdHash, cancellationToken); if (result.IsFailed) return result; @@ -331,4 +331,28 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut public async Task UpdateTenantUserAsync(UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdateTenantUserAsync(request, userId, cancellationToken); + + public async Task> GetPorfoliosAsync(Pagination pagination, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetPorfoliosAsync(pagination, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getportfolios-{pagination.Page}-{pagination.PageSize}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetPorfoliosAsync(pagination, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + + return result.Value; + } + + return Result.Ok(cached.FromJson()); + })); } diff --git a/PostFundManagement.Application/Services/Governance/GovernanceService.cs b/PostFundManagement.Application/Services/Governance/GovernanceService.cs index eb6a375..77749e9 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceService.cs @@ -32,7 +32,7 @@ public sealed class GovernanceService(IDbContextFactory co return Result.Ok(programmes.Select(p => p.Map()).ToArray()); })); - public async Task> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) => + public async Task> GetOrganisationPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var decodeResult = hashService.DecodeLongIdHash(organisationIdHash); @@ -44,7 +44,8 @@ public sealed class GovernanceService(IDbContextFactory co await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var portfolios = await context.Portfolios.AsNoTracking() - .Where(f => f.Id == decodeResult.Value) + .Include(i => i.Organisation) + .Where(f => f.OrganisationId == decodeResult.Value) .OrderByDescending(o => o.CreatedAt) .ToListAsync(cancellationToken); @@ -471,4 +472,17 @@ public sealed class GovernanceService(IDbContextFactory co return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create instrument")); })); + + public async Task> GetPorfoliosAsync(Pagination pagination, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var portfolios = await context.Portfolios.AsNoTracking() + .Skip((pagination.Page - 1) * pagination.PageSize) + .Take(pagination.PageSize) + .ToListAsync(cancellationToken); + + return Result.Ok(portfolios.Select(p => p.Map()).ToArray()); + })); }