Fixed beneficiary outcomes misconfiguration #153
@@ -1,4 +1,4 @@
|
||||
using PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Application.Services.Projects;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
@@ -12,14 +12,7 @@ public sealed class CreateOutcomeEndpoint : IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder builder)
|
||||
{
|
||||
builder.MapPost("api/projects/beneficiaris/outcomes", async (CreateOutcomeRequest request, HttpContext context,
|
||||
IProjectService service, CancellationToken cancellationToken = default) =>
|
||||
await service.CreateOutcomeAsync(request, context.GetUserId(), cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true } => Results.Ok(),
|
||||
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
builder.MapPost("api/projects/beneficiaries/outcomes", HandleCreateAsync)
|
||||
.WithDescription("Add a new beneficiary outcome")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateOutcomeEndpoint).ToEndpointName())
|
||||
@@ -29,6 +22,25 @@ public sealed class CreateOutcomeEndpoint : IEndpoint
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Projects);
|
||||
|
||||
builder.MapPost("api/projects/beneficiaris/outcomes", HandleCreateAsync)
|
||||
.WithDescription("Add a new beneficiary outcome (legacy route)")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(CreateOutcomeEndpoint).ToEndpointName("TypoAlias"))
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces<Problems>(StatusCodes.Status400BadRequest)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.WithTags(EndpointTags.Projects);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleCreateAsync(CreateOutcomeRequest request, HttpContext context,
|
||||
IProjectService service, CancellationToken cancellationToken = default) =>
|
||||
await service.CreateOutcomeAsync(request, context.GetUserId(), cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true } => Results.Ok(),
|
||||
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
@@ -12,15 +12,7 @@ public sealed class GetOutcomeEndpoint : IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder builder)
|
||||
{
|
||||
builder.MapGet("api/projects/outcomes/{outcomeId}", async (string outcomeId, IProjectService service,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await service.GetOutcomeAsync(outcomeId, cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var outcome } => Results.Ok(outcome),
|
||||
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
builder.MapGet("api/projects/outcome/{outcomeId}", HandleGetAsync)
|
||||
.WithDescription("Gets a specific project outcome")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetOutcomeEndpoint).ToEndpointName())
|
||||
@@ -32,5 +24,28 @@ public sealed class GetOutcomeEndpoint : IEndpoint
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Projects)
|
||||
.CacheOutput();
|
||||
|
||||
builder.MapGet("api/projects/outcomes/single/{outcomeId}", HandleGetAsync)
|
||||
.WithDescription("Gets a specific project outcome")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetOutcomeEndpoint).ToEndpointName("Single"))
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Outcome>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces<Problems>(StatusCodes.Status404NotFound)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Projects)
|
||||
.CacheOutput();
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleGetAsync(string outcomeId, IProjectService service,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await service.GetOutcomeAsync(outcomeId, cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var outcome } => Results.Ok(outcome),
|
||||
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Application.Abstractions.Services;
|
||||
using PostFundManagement.Application.Extensions;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Errors;
|
||||
@@ -12,15 +12,7 @@ public sealed class GetOutcomesEndpoint : IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder builder)
|
||||
{
|
||||
builder.MapGet("api/projects/outcomes/{beneficiatyId}", async (string beneficiatyId, IProjectService service,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await service.GetOutcomesAsync(beneficiatyId, cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var outcomes } => Results.Ok(outcomes),
|
||||
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
builder.MapGet("api/projects/beneficiaries/{beneficiaryId}/outcomes", HandleGetAsync)
|
||||
.WithDescription("Gets project beneficiary outcomes")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetOutcomesEndpoint).ToEndpointName())
|
||||
@@ -32,5 +24,41 @@ public sealed class GetOutcomesEndpoint : IEndpoint
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Projects)
|
||||
.CacheOutput();
|
||||
|
||||
builder.MapGet("api/projects/beneficiaris/{beneficiaryId}/outcomes", HandleGetAsync)
|
||||
.WithDescription("Gets project beneficiary outcomes")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetOutcomesEndpoint).ToEndpointName("TypoAlias"))
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Outcome[]>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces<Problems>(StatusCodes.Status404NotFound)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Projects)
|
||||
.CacheOutput();
|
||||
|
||||
builder.MapGet("api/projects/outcomes/{beneficiaryId}", HandleGetAsync)
|
||||
.WithDescription("Gets project beneficiary outcomes")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetOutcomesEndpoint).ToEndpointName("Legacy"))
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Outcome[]>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces<Problems>(StatusCodes.Status404NotFound)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Projects)
|
||||
.CacheOutput();
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleGetAsync(string beneficiaryId, IProjectService service,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await service.GetOutcomesAsync(beneficiaryId, cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var outcomes } => Results.Ok(outcomes),
|
||||
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||
|
||||
[ApiVersionTarget(1)]
|
||||
public sealed class GetProjectOutcomesEndpoint : IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder builder)
|
||||
{
|
||||
builder.MapGet("api/projects/{projectId}/outcomes", async (string projectId, IProjectService service,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await service.GetProjectOutcomesAsync(projectId, cancellationToken) switch
|
||||
{
|
||||
{ IsSuccess: true, Value: var outcomes } => Results.Ok(outcomes),
|
||||
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
|
||||
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
|
||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||
})
|
||||
.WithDescription("Gets project outcomes for all beneficiaries in a project")
|
||||
.RequireAuthorization(Constants.MainScope)
|
||||
.WithName(typeof(GetProjectOutcomesEndpoint).ToEndpointName())
|
||||
.MapToApiVersion(new ApiVersion(1))
|
||||
.Produces<Outcome[]>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces<Problems>(StatusCodes.Status404NotFound)
|
||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||
.WithTags(EndpointTags.Projects)
|
||||
.CacheOutput();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,9 @@ public interface IProjectService
|
||||
[Log]
|
||||
Task<Result<Outcome[]>> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Outcome[]>> GetProjectOutcomesAsync(string projectIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Outcome>> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
@@ -375,6 +375,30 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
|
||||
return Result.Ok(cached.FromJson<Outcome[]>());
|
||||
}));
|
||||
|
||||
public async Task<Result<Outcome[]>> GetProjectOutcomesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result<Outcome[]>>>)(async () =>
|
||||
{
|
||||
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
|
||||
return await service.GetProjectOutcomesAsync(projectIdHash, cancellationToken);
|
||||
|
||||
var cacheKey = HashService.ToMd5Hash($"getprojectoutcomes-{projectIdHash}").Value;
|
||||
|
||||
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cached))
|
||||
{
|
||||
var result = await service.GetProjectOutcomesAsync(projectIdHash, cancellationToken);
|
||||
|
||||
if (result.IsFailed) return result;
|
||||
|
||||
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
|
||||
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
return Result.Ok(cached.FromJson<Outcome[]>());
|
||||
}));
|
||||
|
||||
public async Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result<Programme>>>)(async () =>
|
||||
{
|
||||
|
||||
@@ -834,6 +834,25 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
|
||||
return Result.Ok(outcomes?.Select(o => o.Map()).ToArray() ?? []);
|
||||
}));
|
||||
|
||||
public async Task<Result<Outcome[]>> GetProjectOutcomesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result<Outcome[]>>>)(async () =>
|
||||
{
|
||||
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
|
||||
|
||||
if (decodeResult.IsFailed)
|
||||
return Result.Fail<Outcome[]>(IdDecodeError.Create()
|
||||
.CausedBy(decodeResult.Errors));
|
||||
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var outcomes = await context.Outcomes.AsNoTracking()
|
||||
.Where(o => o.Beneficiary!.ProjectId == decodeResult.Value)
|
||||
.OrderByDescending(o => o.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(outcomes?.Select(o => o.Map()).ToArray() ?? []);
|
||||
}));
|
||||
|
||||
public async Task<Result<Outcome>> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result<Outcome>>>)(async () =>
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1743,14 +1743,41 @@ export const updateBeneficiary = async (payload: UpdateBeneficiaryRequest): Prom
|
||||
|
||||
export const getOutcome = async (outcomeId: string | number): Promise<Outcome> => {
|
||||
const hashedOutcomeId = await toolsService.encodeId(outcomeId);
|
||||
const res = await apiClient.get<Outcome>(`/v1/api/projects/outcomes/${hashedOutcomeId}`);
|
||||
return res.data;
|
||||
try {
|
||||
const res = await apiClient.get<Outcome>(`/v1/api/projects/outcome/${hashedOutcomeId}`);
|
||||
return res.data;
|
||||
} catch {
|
||||
const res = await apiClient.get<Outcome>(`/v1/api/projects/outcomes/single/${hashedOutcomeId}`);
|
||||
return res.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBeneficiaryOutcomes = async (beneficiaryId: string | number): Promise<Outcome[]> => {
|
||||
const hashedBeneficiaryId = await toolsService.encodeId(beneficiaryId);
|
||||
const res = await apiClient.get<Outcome[]>(`/v1/api/projects/outcomes/${hashedBeneficiaryId}`);
|
||||
return res.data || [];
|
||||
try {
|
||||
const res = await apiClient.get<Outcome[]>(`/v1/api/projects/beneficiaries/${hashedBeneficiaryId}/outcomes`);
|
||||
return Array.isArray(res.data) ? res.data : (res.data ? [res.data] : []);
|
||||
} catch (err: any) {
|
||||
if (err?.response?.status === 404) {
|
||||
try {
|
||||
const res = await apiClient.get<Outcome[]>(`/v1/api/projects/outcomes/${hashedBeneficiaryId}`);
|
||||
return Array.isArray(res.data) ? res.data : (res.data ? [res.data] : []);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getProjectOutcomes = async (projectId: string | number): Promise<Outcome[]> => {
|
||||
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
||||
try {
|
||||
const res = await apiClient.get<Outcome[]>(`/v1/api/projects/${hashedProjectId}/outcomes`);
|
||||
return Array.isArray(res.data) ? res.data : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const createOutcome = async (payload: CreateOutcomeRequest): Promise<void> => {
|
||||
@@ -1761,10 +1788,10 @@ export const createOutcome = async (payload: CreateOutcomeRequest): Promise<void
|
||||
status: payload.status !== undefined ? Number(payload.status) : 1,
|
||||
};
|
||||
try {
|
||||
await apiClient.post('/v1/api/projects/beneficiaris/outcomes', body);
|
||||
await apiClient.post('/v1/api/projects/beneficiaries/outcomes', body);
|
||||
} catch (err: any) {
|
||||
if (err?.response?.status === 404) {
|
||||
await apiClient.post('/v1/api/projects/beneficiaries/outcomes', body);
|
||||
await apiClient.post('/v1/api/projects/beneficiaris/outcomes', body);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -78,10 +78,10 @@ export const projectsApi = {
|
||||
apiClient.put<Models.Outcome>('/v1/api/projects/outcomes', payload),
|
||||
|
||||
getOutcome: (outcomeId: string | number) =>
|
||||
apiClient.get<Models.Outcome>(`/v1/api/projects/outcomes/${hashParam(outcomeId)}`),
|
||||
apiClient.get<Models.Outcome>(`/v1/api/projects/outcome/${hashParam(outcomeId)}`),
|
||||
|
||||
getBeneficiaryOutcomes: (beneficiaryId: string | number) =>
|
||||
apiClient.get<Models.Outcome[]>(`/v1/api/projects/outcomes/${hashParam(beneficiaryId)}`),
|
||||
apiClient.get<Models.Outcome[]>(`/v1/api/projects/beneficiaries/${hashParam(beneficiaryId)}/outcomes`),
|
||||
|
||||
// Change Requests
|
||||
createChangeRequest: (payload: Models.CreateChangeRequest) =>
|
||||
|
||||
Reference in New Issue
Block a user