From 9d90f9120bdb3a90d6f310b80e6c23ed99a34c28 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 30 Aug 2026 16:50:40 +0200 Subject: [PATCH 1/2] Implemented GetProjectIndicatorsEndpoint --- .../Evidence/GetMilestoneEvidenceEndpoint.cs | 32 ++++++++ .../Evidence/GetProjectEvidenceEndpoint.cs | 32 ++++++++ .../Evidence/GetProjectIndicatorsEndpoint.cs | 32 ++++++++ .../Services/Evidence/EvidenceService.cs | 74 +++++++++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEvidenceEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Evidence/GetProjectEvidenceEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Evidence/GetProjectIndicatorsEndpoint.cs diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEvidenceEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEvidenceEndpoint.cs new file mode 100644 index 0000000..2381905 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEvidenceEndpoint.cs @@ -0,0 +1,32 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Evidence; + +[ApiVersionTarget(1)] +public class GetMilestoneEvidenceEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/evidence/milestone/{milestoneId}", async (string milestoneId, EvidenceService service, + CancellationToken cancellationToken = default) => + { + var result = await service.GetMilestoneEvidencesAsync(milestoneId, cancellationToken); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.NotFound(); + }) + .WithDescription("Get a list of all uploaded evidence metadata for a specific mliestone") + .WithName(typeof(GetMilestoneEvidenceEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Evidence) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetProjectEvidenceEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/GetProjectEvidenceEndpoint.cs new file mode 100644 index 0000000..474fe46 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Evidence/GetProjectEvidenceEndpoint.cs @@ -0,0 +1,32 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Evidence; + +[ApiVersionTarget(1)] +public class GetProjectEvidenceEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/evidence/milestone/{projectId}", async (string projectId, EvidenceService service, + CancellationToken cancellationToken = default) => + { + var result = await service.GetProjectEvidencesAsync(projectId, cancellationToken); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.NotFound(); + }) + .WithDescription("Get a list of all uploaded evidence metadata for a specific project") + .WithName(typeof(GetProjectEvidenceEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Evidence) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetProjectIndicatorsEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/GetProjectIndicatorsEndpoint.cs new file mode 100644 index 0000000..097ee43 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Evidence/GetProjectIndicatorsEndpoint.cs @@ -0,0 +1,32 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Evidence; + +[ApiVersionTarget(1)] +public class GetProjectIndicatorsEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/evidence/indicators/project/{projectId}", async (string projectId, EvidenceService service, + CancellationToken cancellationToken = default) => + { + var result = await service.GetProjectIndicatorsAsync(projectIdHash: projectId, cancellationToken); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.NotFound(); + }) + .WithDescription("Get get a detailed list of project indicators") + .WithName(typeof(GetProjectIndicatorsEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Evidence); + } +} diff --git a/PostFundManagement.Application/Services/Evidence/EvidenceService.cs b/PostFundManagement.Application/Services/Evidence/EvidenceService.cs index a070297..39944c9 100644 --- a/PostFundManagement.Application/Services/Evidence/EvidenceService.cs +++ b/PostFundManagement.Application/Services/Evidence/EvidenceService.cs @@ -300,4 +300,78 @@ public sealed class EvidenceService(IDbContextFactory cont return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var evidences = await context.Evidences.AsNoTracking() + .Where(e => e.MilestoneId == decodeResult.Value) + .OrderByDescending(o => o.CreatedAt) + .ThenByDescending(o => o.UpdatedAt) + .ToListAsync(cancellationToken); + + return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var evidences = await context.Evidences.AsNoTracking() + .Where(e => e.ProjectId == decodeResult.Value) + .OrderByDescending(o => o.CreatedAt) + .ThenBy(o => o.UpdatedAt) + .ToListAsync(cancellationToken); + + return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var evidences = await context.Indicators.AsNoTracking() + .Where(e => e.ProjectId == decodeResult.Value) + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); + + return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } -- 2.47.3 From 346cb8da5ffeaf91f5dd9001d73164a8eb9e2995 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 30 Aug 2026 17:46:46 +0200 Subject: [PATCH 2/2] Expanded evidence and project endpoints coverage --- .../Endpoints/Projects/GetActivityEndpoint.cs | 31 ++++ .../GetMilestoneEndpoint.cs | 8 +- .../GetMilestonesEndpoint.cs | 10 +- ...tByIdEndpoint.cs => GetProjectEndpoint.cs} | 6 +- .../GetProjectIndicatorsEndpoint.cs | 8 +- .../GetProjectMilestonesEndpoint.cs | 12 +- .../Projects/UpdateMilestoneEndpoint.cs | 31 ++++ .../Projects/UpdateProjectEndpoint.cs | 12 +- .../Projects/UpdateProjectStatusEndpoint.cs | 31 ++++ .../Services/Evidence/EvidenceService.cs | 96 +--------- .../Services/Projects/ProjectService.cs | 169 +++++++++++++++++- .../Services/Projects/Records.cs | 4 + 12 files changed, 295 insertions(+), 123 deletions(-) create mode 100644 PostFundManagement.Api/Endpoints/Projects/GetActivityEndpoint.cs rename PostFundManagement.Api/Endpoints/{Evidence => Projects}/GetMilestoneEndpoint.cs (78%) rename PostFundManagement.Api/Endpoints/{Evidence => Projects}/GetMilestonesEndpoint.cs (75%) rename PostFundManagement.Api/Endpoints/Projects/{GetProjectByIdEndpoint.cs => GetProjectEndpoint.cs} (83%) rename PostFundManagement.Api/Endpoints/{Evidence => Projects}/GetProjectIndicatorsEndpoint.cs (79%) rename PostFundManagement.Api/Endpoints/{Evidence => Projects}/GetProjectMilestonesEndpoint.cs (66%) create mode 100644 PostFundManagement.Api/Endpoints/Projects/UpdateMilestoneEndpoint.cs create mode 100644 PostFundManagement.Api/Endpoints/Projects/UpdateProjectStatusEndpoint.cs diff --git a/PostFundManagement.Api/Endpoints/Projects/GetActivityEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetActivityEndpoint.cs new file mode 100644 index 0000000..ebb83b1 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetActivityEndpoint.cs @@ -0,0 +1,31 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public class GetActivityEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/activities/{activityId}", async (string activityId, + ProjectService service, CancellationToken cancellationToken = default) => + { + var result = await service.GetActivityAsync(activityId, cancellationToken); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.NotFound(); + }) + .WithDescription("Gets a specific activity by it's id") + .WithName(typeof(GetActivityEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetMilestoneEndpoint.cs similarity index 78% rename from PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEndpoint.cs rename to PostFundManagement.Api/Endpoints/Projects/GetMilestoneEndpoint.cs index c1cdcd3..3cc0672 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetMilestoneEndpoint.cs @@ -1,18 +1,18 @@ using PostFundManagement.Application.Extensions; -using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Security; -namespace PostFundManagement.Api.Endpoints.Evidence; +namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] public class GetMilestoneEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/milestones/{milestoneId}", async (string milestoneId, EvidenceService service, + builder.MapGet("api/projects/milestones/{milestoneId}", async (string milestoneId, ProjectService service, CancellationToken cancellationToken = default) => { var result = await service.GetMilestoneAsync(milestoneId, cancellationToken); @@ -27,7 +27,7 @@ public class GetMilestoneEndpoint : IEndpoint .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) - .WithTags(EndpointTags.Evidence) + .WithTags(EndpointTags.Projects) .CacheOutput(); } } diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetMilestonesEndpoint.cs similarity index 75% rename from PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs rename to PostFundManagement.Api/Endpoints/Projects/GetMilestonesEndpoint.cs index cd5bf92..d4ee1af 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetMilestonesEndpoint.cs @@ -1,20 +1,20 @@ using PostFundManagement.Application.Extensions; -using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Security; -namespace PostFundManagement.Api.Endpoints.Evidence; +namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] public class GetMilestonesEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/milestones/{page:int}/{pageSize:int}", async (EvidenceService service, - int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) => + builder.MapGet("api/projects/milestones/{page:int}/{pageSize:int}", async (ProjectService service, + int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => { var result = await service.GetMilestonesAsync(Pagination.Create(page, pageSize), cancellationToken); @@ -29,6 +29,6 @@ public class GetMilestonesEndpoint : IEndpoint .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) - .WithTags(EndpointTags.Evidence); + .WithTags(EndpointTags.Projects); } } diff --git a/PostFundManagement.Api/Endpoints/Projects/GetProjectByIdEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetProjectEndpoint.cs similarity index 83% rename from PostFundManagement.Api/Endpoints/Projects/GetProjectByIdEndpoint.cs rename to PostFundManagement.Api/Endpoints/Projects/GetProjectEndpoint.cs index b2eccb2..e9327d1 100644 --- a/PostFundManagement.Api/Endpoints/Projects/GetProjectByIdEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetProjectEndpoint.cs @@ -8,21 +8,21 @@ using PostFundManagement.Domain.Security; namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] -public class GetProjectByIdEndpoint : IEndpoint +public class GetProjectEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { builder.MapGet("api/projects/{projectId}", async (string projectId, ProjectService service, CancellationToken cancellationToken = default) => { - var result = await service.GetProjectByIdAsync(projectId, cancellationToken); + var result = await service.GetProjectAsync(projectId, cancellationToken); return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(); }) .WithDescription("Get project by ID") - .WithName(typeof(GetProjectByIdEndpoint).ToEndpointName()) + .WithName(typeof(GetProjectEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetProjectIndicatorsEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetProjectIndicatorsEndpoint.cs similarity index 79% rename from PostFundManagement.Api/Endpoints/Evidence/GetProjectIndicatorsEndpoint.cs rename to PostFundManagement.Api/Endpoints/Projects/GetProjectIndicatorsEndpoint.cs index 097ee43..6a2dd56 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetProjectIndicatorsEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetProjectIndicatorsEndpoint.cs @@ -1,18 +1,18 @@ using PostFundManagement.Application.Extensions; -using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Security; -namespace PostFundManagement.Api.Endpoints.Evidence; +namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] public class GetProjectIndicatorsEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/indicators/project/{projectId}", async (string projectId, EvidenceService service, + builder.MapGet("api/projects/indicators/project/{projectId}", async (string projectId, ProjectService service, CancellationToken cancellationToken = default) => { var result = await service.GetProjectIndicatorsAsync(projectIdHash: projectId, cancellationToken); @@ -27,6 +27,6 @@ public class GetProjectIndicatorsEndpoint : IEndpoint .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) - .WithTags(EndpointTags.Evidence); + .WithTags(EndpointTags.Projects); } } diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetProjectMilestonesEndpoint.cs similarity index 66% rename from PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs rename to PostFundManagement.Api/Endpoints/Projects/GetProjectMilestonesEndpoint.cs index 8af68df..3a48bcc 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetProjectMilestonesEndpoint.cs @@ -1,21 +1,21 @@ using PostFundManagement.Application.Extensions; -using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Security; -namespace PostFundManagement.Api.Endpoints.Evidence; +namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] public class GetProjectMilestonesEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/milestones/project/{projectId}", async (string projectId, - EvidenceService service, CancellationToken cancellationToken = default) => + builder.MapGet("api/projects/{projectId}/milestones", async (string projectId, + ProjectService service, CancellationToken cancellationToken = default) => { - var result = await service.GetMilestonesByProjectIdAsync(projectId, cancellationToken); + var result = await service.GetProjectMilestonesAsync(projectId, cancellationToken); return result.IsSuccess ? Results.Ok(result.Value) @@ -27,6 +27,6 @@ public class GetProjectMilestonesEndpoint : IEndpoint .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) - .WithTags(EndpointTags.Evidence); + .WithTags(EndpointTags.Projects); } } diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateMilestoneEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateMilestoneEndpoint.cs new file mode 100644 index 0000000..cf5712e --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateMilestoneEndpoint.cs @@ -0,0 +1,31 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class UpdateMilestoneEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/milestone", async (UpdateMilestoneRequest request, ProjectService service, + HttpContext context, CancellationToken cancellationToken = default) => + { + var result = await service.UpdateMilestoneAsync(request, context.GetUserId(), cancellationToken); + + return result.IsSuccess + ? Results.Ok() + : Results.BadRequest(result.Errors.ToProblems()); + }) + .WithDescription("Update project milestone details") + .WithName(typeof(UpdateMilestoneEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateProjectEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateProjectEndpoint.cs index a2cfbcb..d5a5157 100644 --- a/PostFundManagement.Api/Endpoints/Projects/UpdateProjectEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateProjectEndpoint.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Extensions; @@ -7,20 +7,20 @@ using PostFundManagement.Domain.Security; namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] -public class UpdateProjectEndpoint : IEndpoint +public sealed class UpdateProjectEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapPost("api/projects/status", async (UpdateProjectStatusRequest request, HttpContext context, - ProjectService service, CancellationToken cancellationToken = default) => + builder.MapPut("api/projects", async (UpdateProjectRequest request, + ProjectService service, HttpContext context, CancellationToken cancellationToken = default) => { - var result = await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken); + var result = await service.UpdateProjectAsync(request, userId: context.GetUserId(), cancellationToken); return result.IsSuccess ? Results.Ok() : Results.BadRequest(result.Errors.ToProblems()); }) - .WithDescription("Update project status (M04)") + .WithDescription("Update project details") .WithName(typeof(UpdateProjectEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateProjectStatusEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateProjectStatusEndpoint.cs new file mode 100644 index 0000000..bfaefd6 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateProjectStatusEndpoint.cs @@ -0,0 +1,31 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public class UpdateProjectStatusEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/status", async (UpdateProjectStatusRequest request, HttpContext context, + ProjectService service, CancellationToken cancellationToken = default) => + { + var result = await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken); + + return result.IsSuccess + ? Results.Ok() + : Results.BadRequest(result.Errors.ToProblems()); + }) + .WithDescription("Update project status (M04)") + .WithName(typeof(UpdateProjectStatusEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Application/Services/Evidence/EvidenceService.cs b/PostFundManagement.Application/Services/Evidence/EvidenceService.cs index 39944c9..e084ebc 100644 --- a/PostFundManagement.Application/Services/Evidence/EvidenceService.cs +++ b/PostFundManagement.Application/Services/Evidence/EvidenceService.cs @@ -82,51 +82,7 @@ public sealed class EvidenceService(IDbContextFactory cont return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } - - public async ValueTask> GetMilestonesByProjectIdAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - try - { - var decodeResult = hashService.DecodeLongIdHash(projectIdHash); - - if (decodeResult.IsFailed) - return Result.Fail(decodeResult.Errors); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var milestones = await context.Milestones.AsNoTracking() - .Where(m => m.ProjectId == decodeResult.Value) - .OrderBy(m => m.DueAt) - .ToListAsync(cancellationToken); - - return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } - - public async ValueTask> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var milestones = await context.Milestones.AsNoTracking() - .OrderByDescending(m => m.CreatedAt) - .Skip((pagination.Page - 1) * pagination.PageSize) - .Take(pagination.PageSize) - .ToListAsync(cancellationToken); - - return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } - + public async ValueTask> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) { try @@ -253,31 +209,7 @@ public sealed class EvidenceService(IDbContextFactory cont return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } - - public async Task> GetMilestoneAsync(string milestoneId, CancellationToken cancellationToken) - { - try - { - var decodeResult = hashService.DecodeLongIdHash(milestoneId); - - if (decodeResult.IsFailed) - return Result.Fail(decodeResult.Errors); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var milestone = await context.Milestones.AsNoTracking().FirstOrDefaultAsync(m => m.Id == decodeResult.Value, cancellationToken); - - return milestone is not null - ? Result.Ok(milestone.Map()) - : Result.Fail("Failed to get specific milestone"); - - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } - + public async ValueTask> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) { try @@ -350,28 +282,4 @@ public sealed class EvidenceService(IDbContextFactory cont return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } - - public async Task> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) - { - try - { - var decodeResult = hashService.DecodeLongIdHash(projectIdHash); - - if (decodeResult.IsFailed) - return Result.Fail(decodeResult.Errors); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var evidences = await context.Indicators.AsNoTracking() - .Where(e => e.ProjectId == decodeResult.Value) - .OrderByDescending(o => o.Id) - .ToListAsync(cancellationToken); - - return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } } diff --git a/PostFundManagement.Application/Services/Projects/ProjectService.cs b/PostFundManagement.Application/Services/Projects/ProjectService.cs index 1e5ccac..07c138d 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectService.cs @@ -62,7 +62,7 @@ public sealed class ProjectService(IDbContextFactory conte } } - public async ValueTask> GetProjectByIdAsync(string projectIdHash, CancellationToken cancellationToken = default) + public async ValueTask> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) { try { @@ -179,4 +179,171 @@ public sealed class ProjectService(IDbContextFactory conte return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var milestones = await context.Milestones.AsNoTracking() + .Where(m => m.ProjectId == decodeResult.Value) + .OrderBy(m => m.DueAt) + .ToListAsync(cancellationToken); + + return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var evidences = await context.Indicators.AsNoTracking() + .Where(e => e.ProjectId == decodeResult.Value) + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); + + return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var milestone = await context.Milestones.AsNoTracking().FirstOrDefaultAsync(m => m.Id == decodeResult.Value, cancellationToken); + + return milestone is not null + ? Result.Ok(milestone.Map()) + : Result.Fail("Failed to get specific milestone"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var milestones = await context.Milestones.AsNoTracking() + .OrderByDescending(m => m.CreatedAt) + .Skip((pagination.Page - 1) * pagination.PageSize) + .Take(pagination.PageSize) + .ToListAsync(cancellationToken); + + return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Milestones.Where(m => m.Id == request.MilestoneId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Name, request.Name) + .SetProperty(u => u.DueAt, request.DueAt) + .SetProperty(u => u.Priority, request.Priority) + .SetProperty(u => u.Status, request.Status) + .SetProperty(u => u.UpdatedBy, userId) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return changedTotal > 0 + ? Result.Ok() + : Result.Fail("Failed to update milestone"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Projects.Where(p => p.Id == request.ProjectId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Name, request.Name) + .SetProperty(u => u.Description, request.Description) + .SetProperty(u => u.StartedAt, request.StartedAt) + .SetProperty(u => u.EndedAt, request.EndedAt) + .SetProperty(u => u.UpdatedBy, userId) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow) + .SetProperty(u => u.Sector, request.Sector) + .SetProperty(u => u.ProjectType, request.ProjectType), cancellationToken); + + return changedTotal > 0 + ? Result.Ok() + : Result.Fail("Failed to update project"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(activityIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var activity = await context.Activities.AsNoTracking().FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken); + + return activity is not null + ? Result.Ok(activity.Map()) + : Result.Fail("Failed to get specific activity."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } diff --git a/PostFundManagement.Application/Services/Projects/Records.cs b/PostFundManagement.Application/Services/Projects/Records.cs index 0f3f897..10a9b09 100644 --- a/PostFundManagement.Application/Services/Projects/Records.cs +++ b/PostFundManagement.Application/Services/Projects/Records.cs @@ -2,6 +2,10 @@ namespace PostFundManagement.Application.Services.Projects; +public record UpdateProjectRequest(long ProjectId, string Name, string Description, DateTime StartedAt, DateTime? EndedAt, IndustrySector Sector, ProjectType ProjectType); + +public record UpdateMilestoneRequest(long MilestoneId, DateTime DueAt, string Name, Priority Priority, ApprovalStatus Status); + public record UpdateProjectStatusRequest(long ProjectId, ApprovalStatus Status); public record CreateProjectRequest(long ProgrammeId, string Name, string Description, IndustrySector Sector, ProjectType ProjectType); -- 2.47.3