Added consolidated applicant data retrieval endpoint

This commit is contained in:
2026-10-10 11:11:23 +02:00
parent 2ee7a49122
commit 343f405982
5 changed files with 135 additions and 4 deletions
@@ -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<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Gets an applicant's data")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetIApplicantDataEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Invite>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Communications)
.CacheOutput();
}
}
@@ -33,4 +33,7 @@ public interface ICommunicationsService
[Log]
Task<Result<Notification>> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<ApplicantData[]>> GetApplicantDataAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
}
@@ -12,6 +12,28 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
public async Task<Result> CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateNotificationAsync(request, userId, cancellationToken);
public async Task<Result<ApplicantData[]>> GetApplicantDataAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ApplicantData[]>>>)(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<ApplicantData[]>());
}));
public async Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Invite>>>)(async () =>
{
@@ -9,7 +9,7 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory,
public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory,
HashService hashService, ITenantContextService tenantContext) : ICommunicationsService
{
public async Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
@@ -39,12 +39,12 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
context.Invites.Add(invite);
var notificationRequest = new CreateNotificationRequest(request.InvitedOrganisationId, recipient.UserId,
var notificationRequest = new CreateNotificationRequest(request.InvitedOrganisationId, recipient.UserId,
request.Platform, "Funding award invite", recipient!.User!.Email, request.Message);
var notificationResult = await CreateNotificationAsync(notificationRequest, userId, cancellationToken);
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0 && notificationResult.IsFailed,
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0 && notificationResult.IsFailed,
CreateError.Create("Failed to create invite, contact support").CausedBy(notificationResult.Errors))
.ToResult(invite?.Map()!);
}));
@@ -107,7 +107,7 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
TenantId = tenantContext.TenantId
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 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<ApplicationDbContext
return Result.FailIf(notification is null, NotFoundError.Create("Notification not found"))
.ToResult(notification?.Map()!);
}));
public async Task<Result<ApplicantData[]>> GetApplicantDataAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ApplicantData[]>>>)(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<ApplicantData>();
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());
}));
}
@@ -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; } = [];
}