From 343f405982ff37e3c2c97dbe3c63ebd147473356 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 10 Oct 2026 11:11:23 +0200 Subject: [PATCH] Added consolidated applicant data retrieval endpoint --- .../GetIApplicantDataEndpoint.cs | 35 +++++++++++ .../Services/ICommunicationsService.cs | 3 + .../CommunicationsCacheService.cs | 22 +++++++ .../Communications/CommunicationsService.cs | 63 +++++++++++++++++-- .../Models/ApplicantData.cs | 16 +++++ 5 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 PostFundManagement.Api/Endpoints/Communications/GetIApplicantDataEndpoint.cs create mode 100644 PostFundManagement.Domain/Models/ApplicantData.cs diff --git a/PostFundManagement.Api/Endpoints/Communications/GetIApplicantDataEndpoint.cs b/PostFundManagement.Api/Endpoints/Communications/GetIApplicantDataEndpoint.cs new file mode 100644 index 0000000..9a83ce9 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Communications/GetIApplicantDataEndpoint.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; + +namespace PostFundManagement.Api.Endpoints.Communications; + +[ApiVersionTarget(1)] +public sealed class GetIApplicantDataEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/communications/applicants/{applicantUserId:Guid}/all", async (Guid applicantUserId, + ICommunicationsService service, CancellationToken cancellationtToken = default ) => + await service.GetApplicantDataAsync(applicantUserId, cancellationtToken) switch + { + { IsSuccess: true, Value: var applicantData } => Results.Ok(applicantData), + 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("Gets an applicant's data") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetIApplicantDataEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Communications) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs index 23149ff..3eaf3e6 100644 --- a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs +++ b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs @@ -33,4 +33,7 @@ public interface ICommunicationsService [Log] Task> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default); + + [Log] + Task> GetApplicantDataAsync(Guid applicantUserId, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs b/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs index a4d1300..8a185a4 100644 --- a/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs +++ b/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs @@ -12,6 +12,28 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID public async Task CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.CreateNotificationAsync(request, userId, cancellationToken); + public async Task> GetApplicantDataAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetApplicantDataAsync(applicantUserId, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getapplicantdata-{applicantUserId}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetApplicantDataAsync(applicantUserId, cancellationToken); + + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + } + + return Result.Ok(cached!.FromJson()); + })); + public async Task> GetApplicantInviteAsync(Guid applicantUserId, 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 7151876..90958d2 100644 --- a/PostFundManagement.Application/Services/Communications/CommunicationsService.cs +++ b/PostFundManagement.Application/Services/Communications/CommunicationsService.cs @@ -9,7 +9,7 @@ using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Communications; -public sealed class CommunicationsService(IDbContextFactory contextFactory, +public sealed class CommunicationsService(IDbContextFactory contextFactory, HashService hashService, ITenantContextService tenantContext) : ICommunicationsService { public async Task> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => @@ -39,12 +39,12 @@ public sealed class CommunicationsService(IDbContextFactory 0, + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create entry, contact support")); })); @@ -164,4 +164,59 @@ public sealed class CommunicationsService(IDbContextFactory> GetApplicantDataAsync(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.Id) + .ToListAsync(cancellationToken); + + var applicantData = new List(); + + foreach (var invite in invites) + { + var outputInvite = invite.Map(); + + var contract = (await context.Contracts.AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == invite.ContractId, cancellationToken))?.Map(); + + var fundingOrganisation = (await context.Organisations.AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == invite.OrganisationId, cancellationToken))?.Map(); + + var invitedOrganisation = (await context.Organisations.AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == invite.InvitedOrganisationId, cancellationToken))?.Map(); + + var awards = (await context.Awards.AsNoTracking() + .Where(a => a.Id == invite.AwardId) + .ToListAsync(cancellationToken))?.Select(a => a.Map()).ToArray(); + + var projectIds = (await context.Awards.AsNoTracking() + .Where(a => a.Id == invite.AwardId && a.ProjectId.HasValue) + .Select(a => a.ProjectId!.Value) + .Distinct() + .ToListAsync(cancellationToken)) ?? []; + + var projects = (await context.Projects.AsNoTracking() + .Where(p => projectIds.Contains(p.Id)) + .ToListAsync(cancellationToken))?.Select(p => p.Map()).ToArray() ?? []; + + var data = new ApplicantData + { + Invite = outputInvite, + Contract = contract, + Awards = awards!, + FundingOrganisation = fundingOrganisation, + InvitedOrganisation = invitedOrganisation, + Projects = projects + }; + + applicantData.Add(data); + } + + return Result.Ok(applicantData.ToArray()); + })); } diff --git a/PostFundManagement.Domain/Models/ApplicantData.cs b/PostFundManagement.Domain/Models/ApplicantData.cs new file mode 100644 index 0000000..3033888 --- /dev/null +++ b/PostFundManagement.Domain/Models/ApplicantData.cs @@ -0,0 +1,16 @@ +namespace PostFundManagement.Domain.Models; + +public sealed class ApplicantData +{ + public Invite? Invite { get; set; } + + public Contract? Contract { get; set; } + + public Organisation? FundingOrganisation { get; set; } + + public Organisation? InvitedOrganisation { get; set; } + + public Award[] Awards { get; set; } = []; + + public Project[] Projects { get; set; } = []; +}