Refactored endpoints to use services
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using PostFundManagement.Application.Communications;
|
using PostFundManagement.Application.Communications;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
@@ -14,21 +14,22 @@ public class GetInvitesEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/communications/invites/{page:int}/{pageSize:int}", async (CommunicationsService service, int page = 1, int pageSize = 100,
|
builder.MapGet("api/communications/invites/{page:int}/{pageSize:int}", async (CommunicationsService service,
|
||||||
CancellationToken cancellationToken = default) =>
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var result = await service.GetInvitesAsync(new Pagination { Page = page, PageSize = pageSize }, cancellationToken);
|
var result = await service.GetInvitesAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? Results.Ok(result.Value)
|
? Results.Ok(result.Value)
|
||||||
: Results.BadRequest(result.Errors.ToProblems("Failed to get invites"));
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of sent invitations")
|
.WithDescription("Get a list of sent invitations")
|
||||||
.WithName(typeof(GetInvitesEndpoint).ToEndpointName())
|
.WithName(typeof(GetInvitesEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Invite[]>(StatusCodes.Status200OK)
|
.Produces<Invite[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Communications);
|
.WithTags(EndpointTags.Communications)
|
||||||
|
.CacheOutput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using PostFundManagement.Application.Communications;
|
using PostFundManagement.Application.Communications;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
@@ -10,21 +11,20 @@ public class SendInviteEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/communications/invite", async (InviteRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/communications/invite", async (InviteRequest request,
|
||||||
CommunicationsService service, CancellationToken cancellationToken = default) =>
|
HttpContext context, CommunicationsService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var result = await service.SendInviteAsync(request, principal, cancellationToken);
|
var result = await service.SendInviteAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? Results.Ok(result.Value)
|
? Results.Ok(result.Value)
|
||||||
: Results.BadRequest(result.Errors.ToProblems("Failed to create invite"));
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
|
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
|
||||||
.WithName(typeof(SendInviteEndpoint).ToEndpointName())
|
.WithName(typeof(SendInviteEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Invite>(StatusCodes.Status200OK)
|
.Produces<Domain.Entities.Invite>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Communications);
|
.WithTags(EndpointTags.Communications);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,12 +1,13 @@
|
|||||||
|
using PostFundManagement.Application.Communications.Events;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Events.Communications;
|
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Communications;
|
namespace PostFundManagement.Api.Endpoints.Communications;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class SyncUserEndpoint : IEndpoint
|
public class SyncIdentityUserEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
@@ -19,9 +20,8 @@ public class SyncUserEndpoint : IEndpoint
|
|||||||
|
|
||||||
return Results.Ok();
|
return Results.Ok();
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Trigger integration and data synchronization in the background")
|
.WithDescription("Trigger integration and data synchronization in the background")
|
||||||
.WithName(typeof(SyncUserEndpoint).ToEndpointName())
|
.WithName(typeof(SyncIdentityUserEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using PostFundManagement.Application.Evidence;
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Evidence;
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
@@ -11,21 +11,20 @@ public class CreateIndicatorEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/evidence/indicators", async (CreateIndicatorRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/evidence/indicators", async (CreateIndicatorRequest request, HttpContext context,
|
||||||
EvidenceService service, CancellationToken cancellationToken = default) =>
|
EvidenceService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var result = await service.CreateIndicatorAsync(request, principal, cancellationToken);
|
var result = await service.CreateIndicatorAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? Results.Created($"api/evidence/indicators/{result.Value.Id}", result.Value.Indicator)
|
? Results.Ok()
|
||||||
: Results.BadRequest(result.Errors.ToProblems("Failed to create indicator"));
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Define indicators and baselines/targets (M08)")
|
.WithDescription("Define indicators and baselines/targets (M08)")
|
||||||
.WithName(typeof(CreateIndicatorEndpoint).ToEndpointName())
|
.WithName(typeof(CreateIndicatorEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Indicator>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Evidence);
|
.WithTags(EndpointTags.Evidence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using PostFundManagement.Application.Evidence;
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Evidence;
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
@@ -11,21 +11,20 @@ public class CreateMilestoneEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/evidence/milestones", async (CreateMilestoneRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/evidence/milestones", async (CreateMilestoneRequest request, HttpContext context,
|
||||||
EvidenceService service, CancellationToken cancellationToken = default) =>
|
EvidenceService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var result = await service.CreateMilestoneAsync(request, principal, cancellationToken);
|
var result = await service.CreateMilestoneAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? Results.Created($"api/evidence/milestones/{result.Value.Id}", result.Value.Milestone)
|
? Results.Ok()
|
||||||
: Results.BadRequest(result.Errors.ToProblems("Failed to create a new milestone"));
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Define contractual milestones (M07)")
|
.WithDescription("Define contractual milestones (M07)")
|
||||||
.WithName(typeof(CreateMilestoneRequest).ToEndpointName())
|
.WithName(typeof(CreateMilestoneRequest).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Milestone>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Evidence);
|
.WithTags(EndpointTags.Evidence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using PostFundManagement.Application.Evidence;
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
@@ -10,20 +12,22 @@ public class GetEvidenceFilesEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/evidence/files/{page:int}/{pageSize:int}", async (EvidenceService service, int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
builder.MapGet("api/evidence/files/{page:int}/{pageSize:int}", async (EvidenceService service,
|
||||||
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var result = await service.GetEvidenceFilesAsync(new Domain.Pagination(page, pageSize), cancellationToken);
|
var result = await service.GetEvidenceFilesAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? Results.Ok(result.Value)
|
? Results.Ok(result.Value)
|
||||||
: Results.BadRequest(result.Errors.ToProblems("Failed to get evidence"));
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of all uploaded evidence files metadata")
|
.WithDescription("Get a list of all uploaded evidence files metadata")
|
||||||
.WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName())
|
.WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
|
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Evidence);
|
.WithTags(EndpointTags.Evidence)
|
||||||
|
.CacheOutput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using PostFundManagement.Application.Evidence;
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
@@ -11,19 +13,20 @@ public class GetIndicatorsEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/evidence/indicators/{page:int}/{pageSize:int}", async (EvidenceService service, int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
builder.MapGet("api/evidence/indicators/{page:int}/{pageSize:int}", async (EvidenceService service,
|
||||||
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var result = await service.GetIndicatorsAsync(new Domain.Pagination { Page = page, PageSize = pageSize }, cancellationToken);
|
var result = await service.GetIndicatorsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? Results.Ok(result.Value)
|
? Results.Ok(result.Value)
|
||||||
: Results.BadRequest(result.Errors.ToProblems("Failed to get indicators"));
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of indicators")
|
.WithDescription("Get a list of indicators")
|
||||||
.WithName(typeof(GetIndicatorsEndpoint).ToEndpointName())
|
.WithName(typeof(GetIndicatorsEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Indicator[]>(StatusCodes.Status200OK)
|
.Produces<Indicator[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Evidence);
|
.WithTags(EndpointTags.Evidence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using PostFundManagement.Application.Evidence;
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Models;
|
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Evidence;
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
@@ -14,17 +16,18 @@ public class GetMilestonesEndpoint : IEndpoint
|
|||||||
builder.MapGet("api/evidence/milestones/{page:int}/{pageSize:int}", async (EvidenceService service,
|
builder.MapGet("api/evidence/milestones/{page:int}/{pageSize:int}", async (EvidenceService service,
|
||||||
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var result = await service.GetMilestonesAsync(new Domain.Pagination { Page = page, PageSize = pageSize }, cancellationToken);
|
var result = await service.GetMilestonesAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? Results.Ok(result.Value)
|
? Results.Ok(result.Value)
|
||||||
: Results.BadRequest(result.Errors.ToProblems("Failed to fetch milestones"));
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
.RequireAuthorization()
|
||||||
.WithDescription("Get a list of all milestones")
|
.WithDescription("Get a list of all milestones")
|
||||||
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
|
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Milestone[]>(StatusCodes.Status200OK)
|
.Produces<Milestone[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Evidence);
|
.WithTags(EndpointTags.Evidence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using PostFundManagement.Application.Evidence;
|
using PostFundManagement.Application.Evidence;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Configuration.Entities;
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Evidence;
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ public class GetProjectMilestonesEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/evidence/milestones/project/{projectId:long}", async (long projectId,
|
builder.MapGet("api/evidence/milestones/project/{projectId}", async (string projectId,
|
||||||
EvidenceService service, CancellationToken cancellationToken = default) =>
|
EvidenceService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var result = await service.GetMilestonesByProjectIdAsync(projectId, cancellationToken);
|
var result = await service.GetMilestonesByProjectIdAsync(projectId, cancellationToken);
|
||||||
@@ -20,13 +21,12 @@ public class GetProjectMilestonesEndpoint : IEndpoint
|
|||||||
? Results.Ok(result.Value)
|
? Results.Ok(result.Value)
|
||||||
: Results.NotFound();
|
: Results.NotFound();
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of milestones for a project")
|
.WithDescription("Get a list of milestones for a project")
|
||||||
.WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName())
|
.WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Milestone[]>(StatusCodes.Status200OK)
|
.Produces<Milestone[]>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Evidence);
|
.WithTags(EndpointTags.Evidence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using PostFundManagement.Application.Evidence;
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
@@ -10,21 +11,20 @@ public class RecordIndicatorAmountEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/evidence/indicators/{id:string}/actuals/{actualAmount:decimal}", async (string id, decimal actualAmount,
|
builder.MapPost("api/evidence/indicators/{indicatorId}/actuals/{actualAmount:decimal}", async (string indicatorId, decimal actualAmount,
|
||||||
ClaimsPrincipal principal, EvidenceService service, CancellationToken cancellationToken = default) =>
|
HttpContext context, EvidenceService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var result = await service.RecordIndicatorActualAmountAsync(id, actualAmount, principal, cancellationToken);
|
var result = await service.RecordIndicatorActualAmountAsync(indicatorId, actualAmount, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? Results.Ok()
|
? Results.Ok()
|
||||||
: Results.BadRequest("Failed to record actual amount");
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Record actual values for indicators (M08)")
|
.WithDescription("Record actual values for indicators (M08)")
|
||||||
.WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName())
|
.WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Indicator>(StatusCodes.Status200OK)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Evidence);
|
.WithTags(EndpointTags.Evidence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using PostFundManagement.Application.Evidence;
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
@@ -10,12 +11,10 @@ public class UploadEvidenceFilesEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/evidence/files", async (HttpRequest request, ClaimsPrincipal principal, EvidenceService service, CancellationToken cancellationToken = default) =>
|
builder.MapPost("api/evidence/files", async (HttpRequest request, EvidenceService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var userId = request.HttpContext.GetUserId();
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
|
||||||
|
|
||||||
if (!request.HasFormContentType)
|
if (!request.HasFormContentType)
|
||||||
return Results.BadRequest("Request must be a multipart form.");
|
return Results.BadRequest("Request must be a multipart form.");
|
||||||
@@ -29,8 +28,8 @@ public class UploadEvidenceFilesEndpoint : IEndpoint
|
|||||||
var result = await service.UploadEvidenceFileAsync(userId, form, file, cancellationToken);
|
var result = await service.UploadEvidenceFileAsync(userId, form, file, cancellationToken);
|
||||||
|
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? Results.Created()
|
? Results.Ok()
|
||||||
: Results.BadRequest("Evidence upload failed");
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
.RequireAuthorization()
|
||||||
.DisableAntiforgery()
|
.DisableAntiforgery()
|
||||||
@@ -38,7 +37,7 @@ public class UploadEvidenceFilesEndpoint : IEndpoint
|
|||||||
.WithName(typeof(UploadEvidenceFilesEndpoint).ToEndpointName())
|
.WithName(typeof(UploadEvidenceFilesEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Evidence);
|
.WithTags(EndpointTags.Evidence);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Execution;
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
@@ -11,37 +11,20 @@ public class ApproveDisbursementEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/execution/disbursements/{id:long}/approve", async (long id, ClaimsPrincipal principal,
|
builder.MapPost("api/execution/disbursements/{disbursementId}/approve", async (string disbursementId,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.ApproveDisbursementAsync(disbursementId, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
var disbursement = await context.Disbursements.FirstOrDefaultAsync(d => d.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (disbursement == null)
|
|
||||||
return Results.NotFound($"Disbursement with ID {id} does not exist.");
|
|
||||||
|
|
||||||
disbursement.Status = ApprovalStatus.Approved;
|
|
||||||
disbursement.UpdatedBy = userId;
|
|
||||||
disbursement.UpdatedAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
context.Disbursements.Update(disbursement);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Ok(disbursement)
|
|
||||||
: Results.BadRequest("Failed to approve payment");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Approve disbursement and release payment (M06)")
|
.WithDescription("Approve disbursement and release payment (M06)")
|
||||||
.WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName())
|
.WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Disbursement>(StatusCodes.Status200OK)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Execution);
|
.WithTags(EndpointTags.Execution);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,30 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Execution;
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class CreateDisbursementEndpoint : IEndpoint
|
public class CreateDisbursementEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateDisbursementRequest(long ProjectId, long? MilestoneId, decimal Amount);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/execution/disbursements", async (CreateDisbursementRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/execution/disbursements", async (CreateDisbursementRequest request,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.CreateDisbursementAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
if (request.Amount <= 0)
|
|
||||||
return Results.BadRequest("Amount must be greater than zero.");
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
|
||||||
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
|
|
||||||
|
|
||||||
var disbursement = new Domain.Entities.Disbursement
|
|
||||||
{
|
|
||||||
ProjectId = request.ProjectId,
|
|
||||||
MilestoneId = request.MilestoneId,
|
|
||||||
Amount = request.Amount,
|
|
||||||
Status = ApprovalStatus.Pending,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Disbursements.Add(disbursement);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Created($"api/execution/disbursements/{disbursement.Id}", disbursement)
|
|
||||||
: Results.BadRequest("Failed to create disbursement");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Create a new disbursement schedule (M06)")
|
.WithDescription("Create a new disbursement schedule (M06)")
|
||||||
.WithName(typeof(CreateDisbursementEndpoint).ToEndpointName())
|
.WithName(typeof(CreateDisbursementEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Disbursement>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Execution);
|
.WithTags(EndpointTags.Execution);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,30 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Execution;
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class CreateRiskEndpoint : IEndpoint
|
public class CreateRiskEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateRiskRequest(long ProjectId, RiskLikelihood Likelihood, RiskImpact Impact, string Name, string Description);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/execution/risks", async (CreateRiskRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/execution/risks", async (CreateRiskRequest request, ExecutionService service,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
HttpContext context, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.CreateRiskAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
|
||||||
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
|
|
||||||
|
|
||||||
var risk = new Domain.Entities.Risk
|
|
||||||
{
|
|
||||||
ProjectId = request.ProjectId,
|
|
||||||
Likelihood = request.Likelihood,
|
|
||||||
Impact = request.Impact,
|
|
||||||
Name = request.Name,
|
|
||||||
Description = request.Description,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Risks.Add(risk);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Created($"api/execution/risks/{risk.Id}", risk)
|
|
||||||
: Results.BadRequest("Failed to create project risk");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Add risk to risk register (M09)")
|
.WithDescription("Add risk to risk register (M09)")
|
||||||
.WithName(typeof(CreateRiskEndpoint).ToEndpointName())
|
.WithName(typeof(CreateRiskEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Risk>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Execution);
|
.WithTags(EndpointTags.Execution);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Execution;
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
@@ -10,31 +13,20 @@ public class GetDisbursementsEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/execution/disbursements", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/execution/disbursements", async (ExecutionService service,
|
||||||
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
var result = await service.GetDisbursementsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
if (pageSize < 1) pageSize = 10;
|
|
||||||
if (pageSize > 100) pageSize = 100;
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
var query = context.Disbursements.AsNoTracking();
|
: Results.NotFound();
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var items = await query.OrderByDescending(d => d.CreatedAt)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of disbursements")
|
.WithDescription("Get a list of disbursements")
|
||||||
.WithName(typeof(GetDisbursementsEndpoint).ToEndpointName())
|
.WithName(typeof(GetDisbursementsEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Disbursement[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Execution);
|
.WithTags(EndpointTags.Execution);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using PostFundManagement.Application.Execution;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Execution;
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
@@ -10,23 +12,20 @@ public class GetProjectRisksEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/execution/risks/project/{projectId:long}", async (long projectId,
|
builder.MapGet("api/execution/risks/project/{projectId}", async (string projectId,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
ExecutionService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetProjectRisksAsync(projectId, cancellationToken);
|
||||||
|
|
||||||
var risks = await context.Risks.AsNoTracking()
|
return result.IsSuccess
|
||||||
.Where(r => r.ProjectId == projectId)
|
? Results.Ok(result.Value)
|
||||||
.OrderByDescending(r => r.CreatedAt)
|
: Results.NotFound();
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(risks);
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of risks for a project")
|
.WithDescription("Get a list of risks for a project")
|
||||||
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
|
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Risk[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Execution);
|
.WithTags(EndpointTags.Execution);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Execution;
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
@@ -10,27 +12,15 @@ public class GetRisksEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/execution/risks", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/execution/risks", async (ExecutionService service,
|
||||||
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
var result = await service.GetRisksAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
if (pageSize < 1) pageSize = 10;
|
|
||||||
if (pageSize > 100) pageSize = 100;
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
var query = context.Risks.AsNoTracking();
|
: Results.NotFound();
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var items = await query.OrderByDescending(r => r.CreatedAt)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of all risks")
|
.WithDescription("Get a list of all risks")
|
||||||
.WithName(typeof(GetRisksEndpoint).ToEndpointName())
|
.WithName(typeof(GetRisksEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
|||||||
@@ -1,51 +1,30 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Governance;
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class CreateOrganisationEndpoint : IEndpoint
|
public class CreateOrganisationEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateOrganisationRequest(string Name, string RegistrationNo, string Email, OrganisationType Type);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/governance/organisations", async (CreateOrganisationRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/governance/organisations", async (CreateOrganisationRequest request, HttpContext context,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
GovernanceService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.CreateOrganisationAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
var organisation = new Domain.Entities.Organisation
|
|
||||||
{
|
|
||||||
Name = request.Name,
|
|
||||||
RegistrationNo = request.RegistrationNo,
|
|
||||||
Email = request.Email,
|
|
||||||
Type = request.Type,
|
|
||||||
Status = ActivityStatus.Active,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Organisations.Add(organisation);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Created($"api/governance/organisations/{organisation.Id}", organisation)
|
|
||||||
: Results.BadRequest("Failed to create organisation");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Create a new organization profile")
|
.WithDescription("Create a new organization profile")
|
||||||
.WithName(typeof(CreateOrganisationEndpoint).ToEndpointName())
|
.WithName(typeof(CreateOrganisationEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Organisation>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Governance);
|
.WithTags(EndpointTags.Governance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Governance;
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
@@ -11,43 +11,20 @@ public class DashboardEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/governance/dashboard", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/governance/dashboard", async (GovernanceService service,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetDashboardAsync(cancellationToken);
|
||||||
|
|
||||||
var totalPortfolioValue = await context.Awards.SumAsync(a => a.Amount, cancellationToken);
|
return result.IsSuccess
|
||||||
var activeAwardsCount = await context.Awards.CountAsync(a => a.Status == ApprovalStatus.Approved, cancellationToken);
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
var disbursedAmount = await context.Disbursements.Where(d => d.Status == ApprovalStatus.Approved)
|
|
||||||
.SumAsync(d => d.Amount, cancellationToken);
|
|
||||||
|
|
||||||
var totalProjects = await context.Projects.CountAsync(cancellationToken);
|
|
||||||
var onTrackProjects = await context.Projects.CountAsync(p => p.Status == ApprovalStatus.Approved, cancellationToken);
|
|
||||||
|
|
||||||
var onTrackPercentage = totalProjects > 0
|
|
||||||
? Math.Round((double)onTrackProjects / totalProjects * 100, 1)
|
|
||||||
: 100.0;
|
|
||||||
|
|
||||||
var atRiskCount = await context.Risks.CountAsync(r => r.Impact == RiskImpact.Critical || r.Impact == RiskImpact.Major, cancellationToken);
|
|
||||||
|
|
||||||
var beneficiariesReached = await context.Beneficiaries.SumAsync(b => b.ReachedCount, cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(new
|
|
||||||
{
|
|
||||||
PortfolioValue = totalPortfolioValue,
|
|
||||||
ActiveAwards = activeAwardsCount,
|
|
||||||
Disbursed = disbursedAmount,
|
|
||||||
OnTrackPercentage = onTrackPercentage,
|
|
||||||
AtRiskCount = atRiskCount,
|
|
||||||
BeneficiariesReached = beneficiariesReached
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get executive portfolio dashboard metrics and KPIs")
|
.WithDescription("Get executive portfolio dashboard metrics and KPIs")
|
||||||
.WithName(typeof(DashboardEndpoint).ToEndpointName())
|
.WithName(typeof(DashboardEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Dashboard>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Governance);
|
.WithTags(EndpointTags.Governance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Governance;
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
@@ -10,19 +11,20 @@ public class GetInstrumentsEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/governance/instruments", async (IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
builder.MapGet("api/governance/instruments", async (Application.Governance.GovernanceService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetInstrumentsAsync(cancellationToken);
|
||||||
|
|
||||||
var instruments = await context.Instruments.AsNoTracking().ToListAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
return Results.Ok(instruments);
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get all funding instruments")
|
.WithDescription("Get all funding instruments")
|
||||||
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
|
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<List<Domain.Entities.Instrument>>(StatusCodes.Status200OK)
|
.Produces<Instrument[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Governance);
|
.WithTags(EndpointTags.Governance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Governance;
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
@@ -10,20 +12,19 @@ public class GetOrganisationByIdEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/governance/organisations/{id:long}", async (long id,
|
builder.MapGet("api/governance/organisations/{organisationId}", async (string organisationId,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
GovernanceService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetOrganisationByIdAsync(organisationId, cancellationToken);
|
||||||
|
|
||||||
var organisation = await context.Organisations.AsNoTracking().FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
return organisation != null ? Results.Ok(organisation) : Results.NotFound();
|
: Results.NotFound();
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get organization profile by ID")
|
.WithDescription("Get organization profile by ID")
|
||||||
.WithName(typeof(GetOrganisationByIdEndpoint).ToEndpointName())
|
.WithName(typeof(GetOrganisationByIdEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Organisation>(StatusCodes.Status200OK)
|
.Produces<Organisation>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Governance);
|
.WithTags(EndpointTags.Governance);
|
||||||
|
|||||||
@@ -1,43 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Governance;
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class GetOrganisationsEndpoint : IEndpoint
|
public class GetOrganisationsEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateOrganisationRequest(string Name, string RegistrationNo, string Email, OrganisationType Type);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/governance/organisations", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/governance/organisations", async (GovernanceService service,
|
||||||
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
var result = await service.GetOrganisationsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
if (pageSize < 1) pageSize = 10;
|
|
||||||
if (pageSize > 100) pageSize = 100;
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
var query = context.Organisations.AsNoTracking();
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var items = await query.OrderBy(o => o.Name)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a paginated directory of organizations")
|
.WithDescription("Get a paginated directory of organizations")
|
||||||
.WithName(typeof(GetOrganisationsEndpoint).ToEndpointName())
|
.WithName(typeof(GetOrganisationsEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Organisation[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Governance);
|
.WithTags(EndpointTags.Governance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Governance;
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
@@ -10,20 +12,20 @@ public class GetPortfoliosEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/governance/portfolios", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/governance/portfolios", async (GovernanceService service,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetPorfoliosAsync(cancellationToken);
|
||||||
|
|
||||||
var portfolios = await context.Portfolios.AsNoTracking().ToListAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
return Results.Ok(portfolios);
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get all portfolios")
|
.WithDescription("Get all portfolios")
|
||||||
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
|
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<List<Domain.Entities.Portfolio>>(StatusCodes.Status200OK)
|
.Produces<Portfolio[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Governance);
|
.WithTags(EndpointTags.Governance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Governance;
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
@@ -10,20 +12,20 @@ public class GetProgrammesEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/governance/programmes", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/governance/programmes", async (GovernanceService service,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetProgrammesAsync(cancellationToken);
|
||||||
|
|
||||||
var programmes = await context.Programmes.AsNoTracking().ToListAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
return Results.Ok(programmes);
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get all programmes")
|
.WithDescription("Get all programmes")
|
||||||
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
|
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<List<Domain.Entities.Programme>>(StatusCodes.Status200OK)
|
.Produces<Programme[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Governance);
|
.WithTags(EndpointTags.Governance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,30 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class CreateAwardEndpoint : IEndpoint
|
public class CreateAwardEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateAwardRequest(long OrganisationId, long ProgrammeId, decimal Amount);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/grants/awards", async (CreateAwardRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/grants/awards", async (CreateAwardRequest request, HttpContext context,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.CreateAwardAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
if (request.Amount <= 0)
|
|
||||||
return Results.BadRequest("Approved amount must be greater than zero.");
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (!await context.Organisations.AnyAsync(o => o.Id == request.OrganisationId, cancellationToken))
|
|
||||||
return Results.BadRequest($"Organisation with ID {request.OrganisationId} does not exist.");
|
|
||||||
|
|
||||||
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
|
|
||||||
return Results.BadRequest($"Programme with ID {request.ProgrammeId} does not exist.");
|
|
||||||
|
|
||||||
var award = new Domain.Entities.Award
|
|
||||||
{
|
|
||||||
OrganisationId = request.OrganisationId,
|
|
||||||
ProgrammeId = request.ProgrammeId,
|
|
||||||
Amount = request.Amount,
|
|
||||||
Status = ApprovalStatus.Pending,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Awards.Add(award);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Created($"api/grants/awards/{award.Id}", award)
|
|
||||||
: Results.BadRequest("Failred to create award");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Register an approved funding award (M01)")
|
.WithDescription("Register an approved funding award (M01)")
|
||||||
.WithName(typeof(CreateAwardEndpoint).ToEndpointName())
|
.WithName(typeof(CreateAwardEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Award>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,30 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class CreateBudgetEndpoint : IEndpoint
|
public class CreateBudgetEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateBudgetRequest(long ProjectId, string Name, decimal Amount);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/grants/budgets", async (CreateBudgetRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/grants/budgets", async (CreateBudgetRequest request, HttpContext context,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.CreateBudgetAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
if (request.Amount <= 0)
|
|
||||||
return Results.BadRequest("Budget amount must be greater than zero.");
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
|
||||||
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
|
|
||||||
|
|
||||||
var budget = new Domain.Entities.Budget
|
|
||||||
{
|
|
||||||
ProjectId = request.ProjectId,
|
|
||||||
Name = request.Name,
|
|
||||||
Amount = request.Amount,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Budgets.Add(budget);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Created($"api/grants/budgets/{budget.Id}", budget)
|
|
||||||
: Results.BadRequest("Failed to create budget");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Create a new budget line (M05)")
|
.WithDescription("Create a new budget line (M05)")
|
||||||
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
|
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Budget>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,30 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class CreateContractEndpoint : IEndpoint
|
public class CreateContractEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateContractRequest(long AwardId, decimal TotalValue, DateTime EffectiveAt, DateTime ExpiresAt);
|
|
||||||
public record ExecuteContractRequest(string ExternalSignatureId, string SignedDocumentUrl);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/grants/contracts", async (CreateContractRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/grants/contracts", async (CreateContractRequest request, HttpContext context,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.CreateContractAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
var award = await context.Awards.FirstOrDefaultAsync(a => a.Id == request.AwardId, cancellationToken);
|
|
||||||
|
|
||||||
if (award == null)
|
|
||||||
return Results.BadRequest($"Award with ID {request.AwardId} does not exist.");
|
|
||||||
|
|
||||||
var contract = new Domain.Entities.Contract
|
|
||||||
{
|
|
||||||
AwardId = request.AwardId,
|
|
||||||
TotalValue = request.TotalValue,
|
|
||||||
EffectiveAt = request.EffectiveAt,
|
|
||||||
ExpiresAt = request.ExpiresAt,
|
|
||||||
Status = ContractStatus.Draft,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Contracts.Add(contract);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Created($"api/grants/contracts/{contract.Id}", contract)
|
|
||||||
: Results.BadRequest("Failed to create the contract");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Create a contract record from an approved award (M02)")
|
.WithDescription("Create a contract record from an approved award (M02)")
|
||||||
.WithName(typeof(CreateContractEndpoint).ToEndpointName())
|
.WithName(typeof(CreateContractEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Contract>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,30 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class ExecuteContractEndpoint : IEndpoint
|
public class ExecuteContractEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record ExecuteContractRequest(string ExternalSignatureId, string SignedDocumentUrl);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/grants/contracts/{id:long}/execute", async (long id, ExecuteContractRequest request,
|
builder.MapPost("api/grants/contracts/execute", async (ExecuteContractRequest request, HttpContext context,
|
||||||
ClaimsPrincipal principal, IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.ExecuteContractAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
var contract = await context.Contracts.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
if (contract == null)
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
return Results.NotFound($"Contract with ID {id} does not exist.");
|
|
||||||
|
|
||||||
contract.ExternalSignatureId = request.ExternalSignatureId;
|
|
||||||
contract.SignedDocumentUrl = request.SignedDocumentUrl;
|
|
||||||
contract.Status = ContractStatus.Active;
|
|
||||||
|
|
||||||
context.Contracts.Update(contract);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Ok(contract)
|
|
||||||
: Results.BadRequest("Failed to execute contract");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Execute the contract and lock the contractual version")
|
.WithDescription("Execute the contract and lock the contractual version")
|
||||||
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
|
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Contract>(StatusCodes.Status200OK)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
@@ -10,21 +12,19 @@ public class GetAwardByIdEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/grants/awards/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/grants/awards/{awardId}", async (string awardId, GrantsService service,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetAwardByIdAsync(awardId, cancellationToken);
|
||||||
|
|
||||||
var award = await context.Awards.AsNoTracking()
|
return result.IsSuccess
|
||||||
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
return award != null ? Results.Ok(award) : Results.NotFound();
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get award by ID")
|
.WithDescription("Get award by ID")
|
||||||
.WithName(typeof(GetAwardByIdEndpoint).ToEndpointName())
|
.WithName(typeof(GetAwardByIdEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Award>(StatusCodes.Status200OK)
|
.Produces<Award>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
@@ -10,31 +13,20 @@ public class GetAwardsEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/grants/awards", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/grants/awards", async (GrantsService service,
|
||||||
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
var result = await service.GetAwardsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
if (pageSize < 1) pageSize = 10;
|
|
||||||
if (pageSize > 100) pageSize = 100;
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
var query = context.Awards.AsNoTracking();
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var items = await query.OrderByDescending(a => a.CreatedAt)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of awards")
|
.WithDescription("Get a list of awards")
|
||||||
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
|
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Award[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class GetBudgetByIdEndpoint : IEndpoint
|
public class GetBudgetByIdEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateBudgetRequest(long ProjectId, string Name, decimal Amount);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/grants/budgets/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/grants/budgets/{budgetId}", async (string budgetId, GrantsService service,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetBudgetByIdAsync(budgetId, cancellationToken);
|
||||||
|
|
||||||
var budget = await context.Budgets.AsNoTracking().FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
return budget != null ? Results.Ok(budget) : Results.NotFound();
|
: Results.NotFound();
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get budget by ID")
|
.WithDescription("Get budget by ID")
|
||||||
.WithName(typeof(GetBudgetByIdEndpoint).ToEndpointName())
|
.WithName(typeof(GetBudgetByIdEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Budget>(StatusCodes.Status200OK)
|
.Produces<Budget>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
@@ -10,31 +13,20 @@ public class GetBudgetsEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/grants/budgets", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/grants/budgets", async (GrantsService service,
|
||||||
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
var result = await service.GetBudgetsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
if (pageSize < 1) pageSize = 10;
|
|
||||||
if (pageSize > 100) pageSize = 100;
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
var query = context.Budgets.AsNoTracking();
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var items = await query.OrderByDescending(b => b.CreatedAt)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get all budgets")
|
.WithDescription("Get all budgets")
|
||||||
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
|
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Budget[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class GetContractByIdEndpoint : IEndpoint
|
public class GetContractByIdEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateContractRequest(long AwardId, decimal TotalValue, DateTime EffectiveAt, DateTime ExpiresAt);
|
|
||||||
public record ExecuteContractRequest(string ExternalSignatureId, string SignedDocumentUrl);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/grants/contracts/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/grants/contracts/{contractId}", async (string contractId, GrantsService service,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetContractByIdAsync(contractId, cancellationToken);
|
||||||
var contract = await context.Contracts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
|
|
||||||
return contract != null ? Results.Ok(contract) : Results.NotFound();
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get contract by ID")
|
.WithDescription("Get contract by ID")
|
||||||
.WithName(typeof(GetContractByIdEndpoint).ToEndpointName())
|
.WithName(typeof(GetContractByIdEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Contract>(StatusCodes.Status200OK)
|
.Produces<Contract>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Grants;
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
@@ -10,31 +13,20 @@ public class GetContractsEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/grants/contracts", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/grants/contracts", async (GrantsService service,
|
||||||
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
var result = await service.GetContractsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
if (pageSize < 1) pageSize = 10;
|
|
||||||
if (pageSize > 100) pageSize = 100;
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
var query = context.Contracts.AsNoTracking();
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var items = await query.OrderByDescending(c => c.CreatedAt)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get all contracts")
|
.WithDescription("Get all contracts")
|
||||||
.WithName(typeof(GetContractsEndpoint).ToEndpointName())
|
.WithName(typeof(GetContractsEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Contract[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Grants);
|
.WithTags(EndpointTags.Grants);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,30 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Projects;
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class CreateActivityEndpoint : IEndpoint
|
public class CreateActivityEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateActivityRequest(long? MilestoneId, string Category, decimal Amount, string Title, string Description, DateTime? PerformedAt);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/projects/{projectId:long}/activities", async (long projectId, CreateActivityRequest request,
|
builder.MapPost("api/projects/activities", async (CreateActivityRequest request,
|
||||||
ClaimsPrincipal principal, IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
HttpContext context, ProjectService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.CreateActivityAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (!await context.Projects.AnyAsync(p => p.Id == projectId, cancellationToken))
|
|
||||||
return Results.BadRequest($"Project with ID {projectId} does not exist.");
|
|
||||||
|
|
||||||
if (request.MilestoneId.HasValue)
|
|
||||||
if (!await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken))
|
|
||||||
return Results.BadRequest($"Milestone with ID {request.MilestoneId.Value} does not exist.");
|
|
||||||
|
|
||||||
var activity = new Domain.Entities.Activity
|
|
||||||
{
|
|
||||||
ProjectId = projectId,
|
|
||||||
MilestoneId = request.MilestoneId,
|
|
||||||
Category = request.Category,
|
|
||||||
Amount = request.Amount,
|
|
||||||
Title = request.Title,
|
|
||||||
Description = request.Description,
|
|
||||||
PerformedAt = request.PerformedAt,
|
|
||||||
Status = ApprovalStatus.Pending,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Activities.Add(activity);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Created($"api/projects/{projectId}/activities/{activity.Id}", activity)
|
|
||||||
: Results.BadRequest("Failed to create activity");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Add a new activity under project (M04)")
|
.WithDescription("Add a new activity under project (M04)")
|
||||||
.WithName(typeof(CreateActivityEndpoint).ToEndpointName())
|
.WithName(typeof(CreateActivityEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Activity>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Projects);
|
.WithTags(EndpointTags.Projects);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,30 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Projects;
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class CreateProjectEndpoint : IEndpoint
|
public class CreateProjectEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record CreateProjectRequest(long ProgrammeId, string Name, string Description, IndustrySector Sector, ProjectType ProjectType);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/projects", async (CreateProjectRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/projects", async (CreateProjectRequest request, HttpContext context,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
ProjectService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.CreateProjectAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
|
|
||||||
return Results.BadRequest($"Programme with ID {request.ProgrammeId} does not exist.");
|
|
||||||
|
|
||||||
var project = new Domain.Entities.Project
|
|
||||||
{
|
|
||||||
ProgrammeId = request.ProgrammeId,
|
|
||||||
Name = request.Name,
|
|
||||||
Description = request.Description,
|
|
||||||
Sector = request.Sector,
|
|
||||||
ProjectType = request.ProjectType,
|
|
||||||
Status = ApprovalStatus.Pending,
|
|
||||||
CreatedBy = userId,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Projects.Add(project);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Created($"api/projects/{project.Id}", project)
|
|
||||||
: Results.BadRequest("Failed to create prject");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Create a new project (M04)")
|
.WithDescription("Create a new project (M04)")
|
||||||
.WithName(typeof(CreateProjectEndpoint).ToEndpointName())
|
.WithName(typeof(CreateProjectEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Project>(StatusCodes.Status201Created)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Projects);
|
.WithTags(EndpointTags.Projects);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Projects;
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
@@ -10,23 +11,19 @@ public class GetActivitiesEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/projects/{projectId:long}/activities", async (long projectId,
|
builder.MapGet("api/projects/{projectId}/activities", async (string projectId,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
ProjectService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetActivitiesAsync(projectId, cancellationToken);
|
||||||
|
|
||||||
var activities = await context.Activities.AsNoTracking()
|
return result.IsSuccess
|
||||||
.Where(a => a.ProjectId == projectId)
|
? Results.Ok(result.Value)
|
||||||
.OrderByDescending(a => a.CreatedAt)
|
: Results.NotFound();
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(activities);
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of activities for a project")
|
.WithDescription("Get a list of activities for a project")
|
||||||
.WithName(typeof(GetActivitiesEndpoint).ToEndpointName())
|
.WithName(typeof(GetActivitiesEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Domain.Models.Activity[]>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Projects);
|
.WithTags(EndpointTags.Projects);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Projects;
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
@@ -10,22 +12,19 @@ public class GetProjectByIdEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/projects/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/projects/{projectId}", async (string projectId, ProjectService service,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
var result = await service.GetProjectByIdAsync(projectId, cancellationToken);
|
||||||
|
|
||||||
var project = await context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
return project != null
|
|
||||||
? Results.Ok(project)
|
|
||||||
: Results.NotFound();
|
: Results.NotFound();
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get project by ID")
|
.WithDescription("Get project by ID")
|
||||||
.WithName(typeof(GetProjectByIdEndpoint).ToEndpointName())
|
.WithName(typeof(GetProjectByIdEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Project>(StatusCodes.Status200OK)
|
.Produces<Project[]>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Projects);
|
.WithTags(EndpointTags.Projects);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Projects;
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
@@ -10,31 +13,20 @@ public class GetProjectsEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/projects", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapGet("api/projects", async (ProjectService service,
|
||||||
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
var result = await service.GetProjectsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
if (pageSize < 1) pageSize = 10;
|
|
||||||
if (pageSize > 100) pageSize = 100;
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
var query = context.Projects.AsNoTracking();
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var items = await query.OrderByDescending(p => p.CreatedAt)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Get a list of projects")
|
.WithDescription("Get a list of projects")
|
||||||
.WithName(typeof(GetProjectsEndpoint).ToEndpointName())
|
.WithName(typeof(GetProjectsEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Project[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Projects);
|
.WithTags(EndpointTags.Projects);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,30 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Projects;
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class UpdateProjectEndpoint : IEndpoint
|
public class UpdateProjectEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
public record UpdateProjectStatusRequest(ApprovalStatus Status);
|
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/projects/{id:long}/status", async (long id, UpdateProjectStatusRequest request, ClaimsPrincipal principal,
|
builder.MapPost("api/projects/status", async (UpdateProjectStatusRequest request, HttpContext context,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
ProjectService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
var project = await context.Projects.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (project == null)
|
|
||||||
return Results.NotFound($"Project with ID {id} does not exist.");
|
|
||||||
|
|
||||||
project.Status = request.Status;
|
|
||||||
project.UpdatedBy = userId;
|
|
||||||
project.UpdatedAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
context.Projects.Update(project);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Ok(project)
|
|
||||||
: Results.BadRequest("Failed to update project");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Update project status (M04)")
|
.WithDescription("Update project status (M04)")
|
||||||
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
|
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.Project>(StatusCodes.Status200OK)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Projects);
|
.WithTags(EndpointTags.Projects);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Users;
|
||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Users;
|
namespace PostFundManagement.Api.Endpoints.Users;
|
||||||
|
|
||||||
@@ -11,47 +13,20 @@ public class GetUsersEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/users", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
builder.MapPost("api/users/{page:int}/{pageSize:int}", async (UserFilterRequest request, UserService service,
|
||||||
int page = 1, int pageSize = 10, ActivityStatus? status = null, string? email = null, string?
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
sortBy = null, bool sortDescending = false, CancellationToken cancellationToken = default) =>
|
|
||||||
{
|
{
|
||||||
if (page < 1) page = 1;
|
var result = await service.GetUsersAsync(Pagination.Create(page, pageSize), request, cancellationToken);
|
||||||
if (pageSize < 1) pageSize = 10;
|
|
||||||
if (pageSize > 100) pageSize = 100;
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
var query = context.Users.AsNoTracking();
|
: Results.NotFound();
|
||||||
|
|
||||||
if (status.HasValue)
|
|
||||||
query = query.Where(u => u.Status == status.Value);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(email))
|
|
||||||
query = query.Where(u => u.Email != null && u.Email.Contains(email));
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(sortBy))
|
|
||||||
query = sortBy.ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
"email" => sortDescending ? query.OrderByDescending(u => u.Email) : query.OrderBy(u => u.Email),
|
|
||||||
"lastloginat" => sortDescending ? query.OrderByDescending(u => u.LastLoginAt) : query.OrderBy(u => u.LastLoginAt),
|
|
||||||
"status" => sortDescending ? query.OrderByDescending(u => u.Status) : query.OrderBy(u => u.Status),
|
|
||||||
_ => sortDescending ? query.OrderByDescending(u => u.Id) : query.OrderBy(u => u.Id)
|
|
||||||
};
|
|
||||||
else
|
|
||||||
query = query.OrderBy(u => u.Id);
|
|
||||||
|
|
||||||
var totalCount = await query.CountAsync(cancellationToken);
|
|
||||||
|
|
||||||
var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
|
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
|
||||||
.WithName(typeof(GetUsersEndpoint).ToEndpointName())
|
.WithName(typeof(GetUsersEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<User[]>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Users);
|
.WithTags(EndpointTags.Users);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using PostFundManagement.Domain;
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Users;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Infrastructure.Database;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Users;
|
namespace PostFundManagement.Api.Endpoints.Users;
|
||||||
|
|
||||||
@@ -11,52 +11,20 @@ public class SyncUserEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/users/sync", async (ClaimsPrincipal principal,
|
builder.MapPost("api/users/sync", async (HttpContext context,
|
||||||
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
UserService service, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var result = await service.SyncUserAsync(context.GetUserId(), context.GetUserEmail(), cancellationToken);
|
||||||
var emailClaim = principal.FindFirst("email")?.Value ?? principal.FindFirst(ClaimTypes.Email)?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(sidClaim))
|
return result.IsSuccess
|
||||||
return Results.BadRequest("Missing 'sid' claim in user token.");
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
|
||||||
userId = new Guid(System.Security.Cryptography.MD5.HashData(System.Text.Encoding.UTF8.GetBytes(sidClaim)));
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
|
||||||
var isNew = user == null;
|
|
||||||
|
|
||||||
if (isNew)
|
|
||||||
{
|
|
||||||
user = new Domain.Entities.User
|
|
||||||
{
|
|
||||||
Id = userId,
|
|
||||||
Email = emailClaim,
|
|
||||||
Status = ActivityStatus.Active,
|
|
||||||
LastLoginAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Users.Add(user);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
user!.Email = emailClaim;
|
|
||||||
user.LastLoginAt = DateTime.UtcNow;
|
|
||||||
context.Users.Update(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
|
||||||
? Results.Ok(user)
|
|
||||||
: Results.BadRequest("Failed to sync user");
|
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
|
||||||
.WithDescription("Synchronise caller user profile using id_token's SID, inserting/updating user details in database")
|
.WithDescription("Synchronise caller user profile using id_token's SID, inserting/updating user details in database")
|
||||||
.WithName(typeof(SyncUserEndpoint).ToEndpointName())
|
.WithName(typeof(SyncUserEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces<Domain.Entities.User>(StatusCodes.Status200OK)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Users);
|
.WithTags(EndpointTags.Users);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Shared;
|
||||||
using PostFundManagement.Domain.Api.Configuration;
|
using PostFundManagement.Domain.Api.Configuration;
|
||||||
using PostFundManagement.Domain.Extensions;
|
|
||||||
using PostFundManagement.Domain.Sdk;
|
using PostFundManagement.Domain.Sdk;
|
||||||
using PostFundManagement.Domain.Services.Shared;
|
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Extensions;
|
namespace PostFundManagement.Api.Extensions;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using PostFundManagement.Api.Extensions;
|
using PostFundManagement.Api.Extensions;
|
||||||
|
using PostFundManagement.Application.Api.Filters;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Health;
|
using PostFundManagement.Domain.Health;
|
||||||
using PostFundManagement.Domain.Mediator;
|
using PostFundManagement.Domain.Mediator;
|
||||||
@@ -10,6 +12,7 @@ builder.Services.AddEndpointsApiExplorer()
|
|||||||
.AddApiVersioning(options => options.ApiVersionReader = new HeaderApiVersionReader());
|
.AddApiVersioning(options => options.ApiVersionReader = new HeaderApiVersionReader());
|
||||||
|
|
||||||
builder.Services.AddApiVersioning().AddApiExplorer();
|
builder.Services.AddApiVersioning().AddApiExplorer();
|
||||||
|
builder.Services.AddScoped<ActiveUserFilter>();
|
||||||
|
|
||||||
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
|
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
|
||||||
builder.Services.AddApiServices(builder.Configuration);
|
builder.Services.AddApiServices(builder.Configuration);
|
||||||
@@ -24,6 +27,8 @@ builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineB
|
|||||||
builder.Services.AddQuartzScheduler(DefaultSchedulerName, builder.Configuration);
|
builder.Services.AddQuartzScheduler(DefaultSchedulerName, builder.Configuration);
|
||||||
builder.Services.AddEmailServices(builder.Configuration);
|
builder.Services.AddEmailServices(builder.Configuration);
|
||||||
builder.Services.AddHashServices(builder.Configuration);
|
builder.Services.AddHashServices(builder.Configuration);
|
||||||
|
builder.Services.AddGarageS3(builder.Configuration);
|
||||||
|
builder.Services.AddRegisterServices();
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddDataProtectionDatabase(builder.Configuration);
|
builder.Services.AddDataProtectionDatabase(builder.Configuration);
|
||||||
@@ -57,7 +62,10 @@ ApiVersionSet versionSet = app.NewApiVersionSet("v1")
|
|||||||
|
|
||||||
var versionGroups = new Dictionary<int, RouteGroupBuilder>
|
var versionGroups = new Dictionary<int, RouteGroupBuilder>
|
||||||
{
|
{
|
||||||
{ 1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet) }
|
{
|
||||||
|
1, app.MapGroup("v{version:apiVersion}").WithApiVersionSet(versionSet)
|
||||||
|
.RequireAuthorization().AddEndpointFilter<ActiveUserFilter>()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
app.MapEndpoints(versionGroups);
|
app.MapEndpoints(versionGroups);
|
||||||
|
|||||||
@@ -5,6 +5,18 @@
|
|||||||
"BucketName": "pfm.general",
|
"BucketName": "pfm.general",
|
||||||
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za"
|
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za"
|
||||||
},
|
},
|
||||||
|
"ContractS3Settings": {
|
||||||
|
"ServiceUrl": "http://192.168.1.177:30900",
|
||||||
|
"Region": "garage",
|
||||||
|
"BucketName": "pfm.general",
|
||||||
|
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za"
|
||||||
|
},
|
||||||
|
"EvidenceS3Settings": {
|
||||||
|
"ServiceUrl": "http://192.168.1.177:30900",
|
||||||
|
"Region": "garage",
|
||||||
|
"BucketName": "pfm.general",
|
||||||
|
"CdnBaseUrl": "https://pfm.general.cdn.khongisa.co.za"
|
||||||
|
},
|
||||||
"Monitoring": {
|
"Monitoring": {
|
||||||
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
|
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
|
||||||
"ServiceName": "MidrandBooks.DEV"
|
"ServiceName": "MidrandBooks.DEV"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using static PostFundManagement.Domain.Extensions.Timezones;
|
using static PostFundManagement.Domain.Extensions.Timezones;
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Abstractions;
|
namespace PostFundManagement.Application.Abstractions;
|
||||||
|
|
||||||
public abstract class EventBase
|
public abstract class EventBase
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace PostFundManagement.Application.Abstractions;
|
||||||
|
|
||||||
|
public interface IApplication;
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
using PostFundManagement.Domain.Services;
|
using PostFundManagement.Application.Shared;
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Abstractions;
|
namespace PostFundManagement.Application.Abstractions;
|
||||||
|
|
||||||
public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using PostFundManagement.Application.Shared;
|
||||||
|
using static PostFundManagement.Domain.Extensions.General;
|
||||||
|
using static PostFundManagement.Domain.Extensions.Constants;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Api.Filters;
|
||||||
|
|
||||||
|
public sealed class ActiveUserFilter(IdentityService identityService) : IEndpointFilter
|
||||||
|
{
|
||||||
|
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
||||||
|
{
|
||||||
|
var principal = context.HttpContext.User;
|
||||||
|
|
||||||
|
if (principal.Identity is null || !principal.Identity.IsAuthenticated)
|
||||||
|
return Results.Unauthorized();
|
||||||
|
|
||||||
|
var (decoded, userId) = principal.GetUserId();
|
||||||
|
var (emailDecoded, email) = principal.GetUserEmail();
|
||||||
|
|
||||||
|
if (!decoded)
|
||||||
|
return Results.Json(new { message = "Invalid user identifier in token." }, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
|
if (!emailDecoded)
|
||||||
|
return Results.Json(new { message = "Invalid/missing user email in token." }, statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
|
if ((await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted)).IsFailed)
|
||||||
|
return (object?)Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
|
||||||
|
|
||||||
|
context.HttpContext.Items[UserIdItemKey] = userId;
|
||||||
|
context.HttpContext.Items[EmaiItemKey] = email;
|
||||||
|
|
||||||
|
return await next(context);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,15 +8,10 @@ namespace PostFundManagement.Application.Communications;
|
|||||||
|
|
||||||
public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
|
public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
|
||||||
{
|
{
|
||||||
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken = default)
|
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var currentUserId))
|
|
||||||
return Result.Fail("Missing or invalid 'sid' claim.");
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
var invite = new Domain.Entities.Invite
|
var invite = new Domain.Entities.Invite
|
||||||
@@ -26,7 +21,7 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
|
|||||||
AwardId = request.AwardId,
|
AwardId = request.AwardId,
|
||||||
ContractId = request.ContractId,
|
ContractId = request.ContractId,
|
||||||
Recipient = request.Recipient,
|
Recipient = request.Recipient,
|
||||||
CreatedBy = currentUserId,
|
CreatedBy = userId,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,13 +54,13 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
|
|||||||
{
|
{
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
var envites = await context.Invites.AsNoTracking()
|
var invites = await context.Invites.AsNoTracking()
|
||||||
.OrderByDescending(i => i.CreatedAt)
|
.OrderByDescending(i => i.CreatedAt)
|
||||||
.Skip((pagination.Page - 1) * pagination.PageSize)
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
.Take(pagination.PageSize)
|
.Take(pagination.PageSize)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return Result.Ok(envites.Select(i => i.Map()).ToArray());
|
return Result.Ok(invites?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Communications.Events;
|
||||||
|
|
||||||
|
public class SyncUserEvent : EventBase, IEvent
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = nameof(SyncUserEvent);
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using PostFundManagement.Domain.Events.Communications;
|
namespace PostFundManagement.Application.Communications.Events;
|
||||||
|
|
||||||
namespace PostFundManagement.Application.Communications.Events;
|
|
||||||
|
|
||||||
public class SyncUserEventHandler(ILogger<SyncUserEvent> logger) : INotificationHandler<SyncUserEvent>
|
public class SyncUserEventHandler(ILogger<SyncUserEvent> logger) : INotificationHandler<SyncUserEvent>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using PostFundManagement.Infrastructure.Database;
|
|||||||
namespace PostFundManagement.Application.Evidence;
|
namespace PostFundManagement.Application.Evidence;
|
||||||
|
|
||||||
public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
|
public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
|
||||||
[FromKeyedServices(Constants.EvidenceS3SettingsSection)] IS3Service s3Service) : IService
|
[FromKeyedServices(Constants.EvidenceBucketName)] IS3Service s3Service) : IService
|
||||||
{
|
{
|
||||||
public async ValueTask<Result> UploadEvidenceFileAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default)
|
public async ValueTask<Result> UploadEvidenceFileAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -57,24 +57,18 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string hash, decimal amount, ClaimsPrincipal principal, CancellationToken cancellationToken = default)
|
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash);
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
if (decodeResult.IsFailed)
|
||||||
return Result.Fail("Missing or invalid 'sid' claim.");
|
return Result.Fail(decodeResult.Errors);
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
var hashResult = hashService.DecodeLongIdHash(hash);
|
var totalUpdated = await context.Indicators.Where(i => i.Id == decodeResult.Value)
|
||||||
long indicatorId = 0;
|
|
||||||
|
|
||||||
if (hashResult.IsFailed)
|
|
||||||
return Result.Fail(hashResult.Errors);
|
|
||||||
|
|
||||||
var totalUpdated = await context.Indicators.Where(i => i.Id == indicatorId)
|
|
||||||
.ExecuteUpdateAsync(f => f
|
.ExecuteUpdateAsync(f => f
|
||||||
.SetProperty(f => f.ActualAmount, amount)
|
.SetProperty(f => f.ActualAmount, amount)
|
||||||
.SetProperty(f => f.CreatedBy, userId), cancellationToken);
|
.SetProperty(f => f.CreatedBy, userId), cancellationToken);
|
||||||
@@ -89,18 +83,23 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result<Milestone[]>> GetMilestonesByProjectIdAsync(long projectId, CancellationToken cancellationToken = default)
|
public async ValueTask<Result<Milestone[]>> GetMilestonesByProjectIdAsync(string projectIdHash, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
|
||||||
|
|
||||||
|
if (decodeResult.IsFailed)
|
||||||
|
return Result.Fail(decodeResult.Errors);
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
var milestones = await context.Milestones.AsNoTracking()
|
var milestones = await context.Milestones.AsNoTracking()
|
||||||
.Where(m => m.ProjectId == projectId)
|
.Where(m => m.ProjectId == decodeResult.Value)
|
||||||
.OrderBy(m => m.DueAt)
|
.OrderBy(m => m.DueAt)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return Result.Ok(milestones.Select(m => m.Map()).ToArray());
|
return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -120,9 +119,7 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
|||||||
.Take(pagination.PageSize)
|
.Take(pagination.PageSize)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return milestones?.Count > 0
|
return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
? Result.Ok(milestones.Select(i => i.Map()).ToArray())
|
|
||||||
: Result.Ok<Milestone[]>([]);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -142,9 +139,7 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
|||||||
.Take(pagination.PageSize)
|
.Take(pagination.PageSize)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return indicators?.Count > 0
|
return Result.Ok(indicators?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
? Result.Ok(indicators.Select(i => i.Map()).ToArray())
|
|
||||||
: Result.Ok<Indicator[]>([]);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -164,9 +159,7 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
|||||||
.Take(pagination.PageSize)
|
.Take(pagination.PageSize)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return evidence?.Count > 0
|
return Result.Ok(evidence?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
? Result.Ok(evidence.Select(e => e.Map()).ToArray())
|
|
||||||
: Result.Ok<Domain.Models.Evidence[]>([]);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -174,21 +167,16 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result<(string Id, Milestone Milestone)>> CreateMilestoneAsync(CreateMilestoneRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken = default)
|
public async ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
|
||||||
return Result.Fail("Missing or invalid 'sid' claim.");
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
||||||
return Result.Fail($"Project with ID {request.ProjectId} does not exist.");
|
return Result.Fail($"Project with ID {request.ProjectId} does not exist.");
|
||||||
|
|
||||||
var milestone = new Domain.Entities.Milestone
|
context.Milestones.Add(new Domain.Entities.Milestone
|
||||||
{
|
{
|
||||||
ProjectId = request.ProjectId,
|
ProjectId = request.ProjectId,
|
||||||
DueAt = request.DueAt,
|
DueAt = request.DueAt,
|
||||||
@@ -197,12 +185,10 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
|||||||
Status = ApprovalStatus.Pending,
|
Status = ApprovalStatus.Pending,
|
||||||
CreatedBy = userId,
|
CreatedBy = userId,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
});
|
||||||
|
|
||||||
context.Milestones.Add(milestone);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
? Result.Ok((hashService.HashEncodeLongId(milestone.Id).Value, milestone.Map()))
|
? Result.Ok()
|
||||||
: Result.Fail("Failed to create the milestone");
|
: Result.Fail("Failed to create the milestone");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -211,21 +197,19 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result<(string Id, Indicator Indicator)>> CreateIndicatorAsync(CreateIndicatorRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken = default)
|
public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
|
||||||
|
|
||||||
if (!Guid.TryParse(sidClaim, out var userId))
|
|
||||||
return Result.Fail("Missing or invalid 'sid' claim.");
|
|
||||||
|
|
||||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Users.AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken))
|
||||||
|
return Result.Fail("Invalid user");
|
||||||
|
|
||||||
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
||||||
return Result.Fail($"Project with ID {request.ProjectId} does not exist.");
|
return Result.Fail($"Project with ID {request.ProjectId} does not exist.");
|
||||||
|
|
||||||
var indicator = new Domain.Entities.Indicator
|
context.Indicators.Add(new Domain.Entities.Indicator
|
||||||
{
|
{
|
||||||
ProjectId = request.ProjectId,
|
ProjectId = request.ProjectId,
|
||||||
Name = request.Name,
|
Name = request.Name,
|
||||||
@@ -235,12 +219,10 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
|
|||||||
ActualAmount = request.BaselineAmount,
|
ActualAmount = request.BaselineAmount,
|
||||||
CreatedBy = userId,
|
CreatedBy = userId,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
};
|
});
|
||||||
|
|
||||||
context.Indicators.Add(indicator);
|
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
? Result.Ok((hashService.HashEncodeLongId(indicator.Id).Value, indicator.Map()))
|
? Result.Ok()
|
||||||
: Result.Fail("Failed to create the invite");
|
: Result.Fail("Failed to create the invite");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using PostFundManagement.Application.Shared;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Execution;
|
||||||
|
|
||||||
|
public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
|
||||||
|
{
|
||||||
|
public async Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var risks = await context.Risks.AsNoTracking()
|
||||||
|
.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(risks?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(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 risks = await context.Risks.AsNoTracking()
|
||||||
|
.Where(r => r.ProjectId == decodeResult.Value)
|
||||||
|
.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(risks?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var disbursements = await context.Disbursements.AsNoTracking()
|
||||||
|
.OrderByDescending(d => d.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(disbursements?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
||||||
|
return Result.Fail($"Project with ID {request.ProjectId} does not exist.");
|
||||||
|
|
||||||
|
context.Risks.Add(new Domain.Entities.Risk
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
Likelihood = request.Likelihood,
|
||||||
|
Impact = request.Impact,
|
||||||
|
Name = request.Name,
|
||||||
|
Description = request.Description,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create project risk");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
||||||
|
return Result.Fail($"Project with ID {request.ProjectId} does not exist.");
|
||||||
|
|
||||||
|
context.Disbursements.Add(new Domain.Entities.Disbursement
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
MilestoneId = request.MilestoneId,
|
||||||
|
Amount = request.Amount,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create disbursement");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash);
|
||||||
|
|
||||||
|
if (decodeResult.IsFailed)
|
||||||
|
return Result.Fail("Invalid disbursementId");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var totalSaved = await context.Disbursements.Where(d => d.Id == decodeResult.Value)
|
||||||
|
.ExecuteUpdateAsync(f =>
|
||||||
|
f.SetProperty(f => f.Status, ApprovalStatus.Approved)
|
||||||
|
.SetProperty(f => f.UpdatedBy, userId)
|
||||||
|
.SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken);
|
||||||
|
|
||||||
|
return totalSaved > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to approve payment");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Execution;
|
||||||
|
|
||||||
|
public record CreateDisbursementRequest(long ProjectId, long? MilestoneId, decimal Amount);
|
||||||
|
|
||||||
|
public record CreateRiskRequest(long ProjectId, RiskLikelihood Likelihood, RiskImpact Impact, string Name, string Description);
|
||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Api.Configuration;
|
using PostFundManagement.Domain.Api.Configuration;
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Extensions;
|
namespace PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
public static class Api
|
public static class Api
|
||||||
{
|
{
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
|
using PostFundManagement.Application.Shared;
|
||||||
using PostFundManagement.Domain.Configuration.Email;
|
using PostFundManagement.Domain.Configuration.Email;
|
||||||
using PostFundManagement.Domain.Services.Shared;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Extensions;
|
namespace PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
public static class Email
|
public static class Email
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
|
public static class General
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddRegisterServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
var serviceType = typeof(IService);
|
||||||
|
|
||||||
|
var sharedImplementations = typeof(IApplication).Assembly.GetTypes()
|
||||||
|
.Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
|
||||||
|
|
||||||
|
foreach (var sharedImplementation in sharedImplementations) services.AddScoped(sharedImplementation);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
|
using PostFundManagement.Application.Shared;
|
||||||
using PostFundManagement.Domain.Configuration.Hash;
|
using PostFundManagement.Domain.Configuration.Hash;
|
||||||
using PostFundManagement.Domain.Services;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Extensions;
|
namespace PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
public static class Hash
|
public static class Hash
|
||||||
{
|
{
|
||||||
+6
-6
@@ -1,8 +1,8 @@
|
|||||||
|
using PostFundManagement.Application.Shared;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Services.Shared;
|
|
||||||
using static PostFundManagement.Domain.Extensions.Constants;
|
using static PostFundManagement.Domain.Extensions.Constants;
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Extensions;
|
namespace PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
public static class S3
|
public static class S3
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,7 @@ public static class S3
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value))
|
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value))
|
||||||
{
|
{
|
||||||
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(EvidenceS3SettingsSection, (provider, client) =>
|
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(EvidenceBucketName, (provider, client) =>
|
||||||
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{EvidenceS3SettingsSection}:AccessKey").Value,
|
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{EvidenceS3SettingsSection}:AccessKey").Value,
|
||||||
configuration.GetSection($"{EvidenceS3SettingsSection}:SecretKey").Value),
|
configuration.GetSection($"{EvidenceS3SettingsSection}:SecretKey").Value),
|
||||||
new AmazonS3Config
|
new AmazonS3Config
|
||||||
@@ -39,12 +39,12 @@ public static class S3
|
|||||||
UseHttp = configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase),
|
UseHttp = configuration.GetSection($"{EvidenceS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
services.AddKeyedScoped<IS3Service, EvidenceS3Service>(EvidenceS3SettingsSection);
|
services.AddKeyedScoped<IS3Service, EvidenceS3Service>(EvidenceBucketName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value))
|
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value))
|
||||||
{
|
{
|
||||||
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(ContractS3SettingsSection, (provider, client) =>
|
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(ContractBucketName, (provider, client) =>
|
||||||
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{ContractS3SettingsSection}:AccessKey").Value,
|
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{ContractS3SettingsSection}:AccessKey").Value,
|
||||||
configuration.GetSection($"{ContractS3SettingsSection}:SecretKey").Value),
|
configuration.GetSection($"{ContractS3SettingsSection}:SecretKey").Value),
|
||||||
new AmazonS3Config
|
new AmazonS3Config
|
||||||
@@ -56,7 +56,7 @@ public static class S3
|
|||||||
UseHttp = configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase),
|
UseHttp = configuration.GetSection($"{ContractS3SettingsSection}:ServiceUrl").Value!.Contains("http://", StringComparison.InvariantCultureIgnoreCase),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
services.AddKeyedScoped<IS3Service, ContractS3Service>(ContractS3SettingsSection);
|
services.AddKeyedScoped<IS3Service, ContractS3Service>(ContractBucketName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
using PostFundManagement.Application.Shared;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Governance;
|
||||||
|
|
||||||
|
public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
|
||||||
|
{
|
||||||
|
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var programmes = await context.Programmes.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(programmes?.Select(p => p.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var portfolios = await context.Portfolios.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(portfolios?.Select(p => p.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var organisations = await context.Organisations.AsNoTracking()
|
||||||
|
.OrderBy(o => o.Name)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(organisations?.Select(o => o.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Organisation>> GetOrganisationByIdAsync(string organisationIdHash, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decodeResult = hashService.DecodeLongIdHash(organisationIdHash);
|
||||||
|
|
||||||
|
if (decodeResult.IsFailed)
|
||||||
|
return Result.Fail(decodeResult.Errors);
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var organisation = await context.Organisations.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken);
|
||||||
|
|
||||||
|
return organisation is not null
|
||||||
|
? Result.Ok(organisation.Map())
|
||||||
|
: Result.Fail("Failed to find specific organisation");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var instruments = await context.Instruments.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(instruments?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var totalPortfolioValue = await context.Awards.SumAsync(a => a.Amount, cancellationToken);
|
||||||
|
var activeAwardsCount = await context.Awards.CountAsync(a => a.Status == ApprovalStatus.Approved, cancellationToken);
|
||||||
|
|
||||||
|
var disbursedAmount = await context.Disbursements.Where(d => d.Status == ApprovalStatus.Approved)
|
||||||
|
.SumAsync(d => d.Amount, cancellationToken);
|
||||||
|
|
||||||
|
var totalProjects = await context.Projects.CountAsync(cancellationToken);
|
||||||
|
var onTrackProjects = await context.Projects.CountAsync(p => p.Status == ApprovalStatus.Approved, cancellationToken);
|
||||||
|
|
||||||
|
var onTrackPercentage = totalProjects > 0
|
||||||
|
? Math.Round((double)onTrackProjects / totalProjects * 100, 1)
|
||||||
|
: 100.0;
|
||||||
|
|
||||||
|
var atRiskCount = await context.Risks.CountAsync(r => r.Impact == RiskImpact.Critical || r.Impact == RiskImpact.Major, cancellationToken);
|
||||||
|
|
||||||
|
var beneficiariesReached = await context.Beneficiaries.SumAsync(b => b.ReachedCount, cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(new Dashboard(totalPortfolioValue, activeAwardsCount,
|
||||||
|
disbursedAmount, onTrackPercentage, atRiskCount, beneficiariesReached));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (await context.Organisations.AnyAsync(o => o.RegistrationNo == request.RegistrationNo, cancellationToken))
|
||||||
|
return Result.Fail("Organisation with the same registration number already exists");
|
||||||
|
|
||||||
|
context.Organisations.Add(new Domain.Entities.Organisation
|
||||||
|
{
|
||||||
|
Name = request.Name,
|
||||||
|
RegistrationNo = request.RegistrationNo,
|
||||||
|
Email = request.Email,
|
||||||
|
Type = request.Type,
|
||||||
|
Status = ActivityStatus.Active,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create organisation");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Governance;
|
||||||
|
|
||||||
|
public record CreateOrganisationRequest(string Name, string RegistrationNo, string Email, OrganisationType Type);
|
||||||
|
|
||||||
|
public record Dashboard(decimal PortfolioValue, int ActiveAwards, decimal Disbursed, double OnTrackPercentage, int AtRiskCount, int BeneficiariesReached);
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
using PostFundManagement.Application.Shared;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Grants;
|
||||||
|
|
||||||
|
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
|
||||||
|
{
|
||||||
|
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var contracts = await context.Contracts.AsNoTracking()
|
||||||
|
.OrderByDescending(c => c.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(contracts?.Select(c => c.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Contract>> GetContractByIdAsync(string contractIdHash, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
|
||||||
|
|
||||||
|
if (decodeResult.IsFailed)
|
||||||
|
return Result.Fail(decodeResult.Errors);
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var contract = await context.Contracts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken);
|
||||||
|
|
||||||
|
return contract is not null
|
||||||
|
? Result.Ok(contract.Map())
|
||||||
|
: Result.Fail("Failed to find specific contract");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var budgets = await context.Budgets.AsNoTracking()
|
||||||
|
.OrderByDescending(b => b.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(budgets?.Select(b => b.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Budget>> GetBudgetByIdAsync(string budgetIdHash, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decodeResult = hashService.DecodeLongIdHash(budgetIdHash);
|
||||||
|
|
||||||
|
if (decodeResult.IsFailed)
|
||||||
|
return Result.Fail(decodeResult.Errors);
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var budget = await context.Budgets.AsNoTracking().FirstOrDefaultAsync(b => b.Id == decodeResult.Value, cancellationToken);
|
||||||
|
|
||||||
|
return budget is not null
|
||||||
|
? Result.Ok(budget.Map())
|
||||||
|
: Result.Fail("Failed to get specific budget.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var awards = await context.Awards.AsNoTracking()
|
||||||
|
.OrderByDescending(a => a.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(awards?.Select(a => a.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Award>> GetAwardByIdAsync(string awardIdHash, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decodeResult = hashService.DecodeLongIdHash(awardIdHash);
|
||||||
|
|
||||||
|
if (decodeResult.IsFailed)
|
||||||
|
return Result.Fail(decodeResult.Errors);
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var award = await context.Awards.AsNoTracking().FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken);
|
||||||
|
|
||||||
|
return award is not null
|
||||||
|
? Result.Ok(award.Map())
|
||||||
|
: Result.Fail("Award not found");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var changedTotal = await context.Contracts
|
||||||
|
.Where(c => c.Id == request.ContractId && c.Status != ContractStatus.Active)
|
||||||
|
.ExecuteUpdateAsync(f => f
|
||||||
|
.SetProperty(u => u.ExternalSignatureId, request.ExternalSignatureId)
|
||||||
|
.SetProperty(u => u.SignedDocumentUrl, request.SignedDocumentUrl)
|
||||||
|
.SetProperty(u => u.CreatedBy, userId)
|
||||||
|
.SetProperty(u => u.Status, ContractStatus.Active), cancellationToken);
|
||||||
|
|
||||||
|
return changedTotal > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Contract activation failed");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var award = await context.Awards.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == request.AwardId, cancellationToken);
|
||||||
|
|
||||||
|
if (award == null)
|
||||||
|
return Result.Fail($"Award with ID {request.AwardId} does not exist.");
|
||||||
|
|
||||||
|
var contract = new Domain.Entities.Contract
|
||||||
|
{
|
||||||
|
AwardId = request.AwardId,
|
||||||
|
TotalValue = request.TotalValue,
|
||||||
|
EffectiveAt = request.EffectiveAt,
|
||||||
|
ExpiresAt = request.ExpiresAt,
|
||||||
|
Status = ContractStatus.Draft,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Contracts.Add(contract);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create contract");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (request.Amount <= 0)
|
||||||
|
return Result.Fail("Budget amount must be greater than zero.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
||||||
|
return Result.Fail($"Project with ID {request.ProjectId} does not exist.");
|
||||||
|
|
||||||
|
var budget = new Domain.Entities.Budget
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
Name = request.Name,
|
||||||
|
Amount = request.Amount,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Budgets.Add(budget);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create budget");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (request.Amount <= 0)
|
||||||
|
return Result.Fail("Approved amount must be greater than zero.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Organisations.AnyAsync(o => o.Id == request.OrganisationId, cancellationToken))
|
||||||
|
return Result.Fail($"Organisation with ID {request.OrganisationId} does not exist.");
|
||||||
|
|
||||||
|
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
|
||||||
|
return Result.Fail($"Programme with ID {request.ProgrammeId} does not exist.");
|
||||||
|
|
||||||
|
var award = new Domain.Entities.Award
|
||||||
|
{
|
||||||
|
OrganisationId = request.OrganisationId,
|
||||||
|
ProgrammeId = request.ProgrammeId,
|
||||||
|
Amount = request.Amount,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Awards.Add(award);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create award.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PostFundManagement.Application.Grants;
|
||||||
|
|
||||||
|
public record CreateAwardRequest(long OrganisationId, long ProgrammeId, decimal Amount);
|
||||||
|
|
||||||
|
public record CreateBudgetRequest(long ProjectId, string Name, decimal Amount);
|
||||||
|
|
||||||
|
public record CreateContractRequest(long AwardId, decimal TotalValue, DateTime EffectiveAt, DateTime ExpiresAt);
|
||||||
|
|
||||||
|
public record ExecuteContractRequest(long ContractId, string ExternalSignatureId, string SignedDocumentUrl);
|
||||||
@@ -14,8 +14,19 @@
|
|||||||
<ProjectReference Include="..\PostFundManagement.Infrastructure\PostFundManagement.Infrastructure.csproj" />
|
<ProjectReference Include="..\PostFundManagement.Infrastructure\PostFundManagement.Infrastructure.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- API Documentation -->
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
|
||||||
|
<PackageReference Include="Scalar.AspNetCore" Version="2.17.1" />
|
||||||
|
|
||||||
|
<Using Include="Scalar.AspNetCore" />
|
||||||
|
<Using Include="Microsoft.OpenApi" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.OpenApi" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Using Include="Asp.Versioning" />
|
||||||
<Using Include="System.Security.Claims" />
|
<Using Include="System.Security.Claims" />
|
||||||
<Using Include="System.Diagnostics" />
|
<Using Include="System.Diagnostics" />
|
||||||
<Using Include="MimeKit" />
|
<Using Include="MimeKit" />
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
using PostFundManagement.Application.Shared;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Projects;
|
||||||
|
|
||||||
|
public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IService
|
||||||
|
{
|
||||||
|
public async ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var project = await context.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, cancellationToken);
|
||||||
|
|
||||||
|
if (project == null)
|
||||||
|
return Result.Fail("Project does not exist.");
|
||||||
|
|
||||||
|
project.Status = request.Status;
|
||||||
|
project.UpdatedBy = userId;
|
||||||
|
project.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
context.Projects.Update(project);
|
||||||
|
|
||||||
|
var changedTotal = await context.Projects
|
||||||
|
.Where(p => p.Id != request.ProjectId)
|
||||||
|
.ExecuteUpdateAsync(f => f
|
||||||
|
.SetProperty(p => p.Status, request.Status)
|
||||||
|
.SetProperty(p => p.UpdatedBy, userId)
|
||||||
|
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), 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<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var projects = await context.Projects.AsNoTracking().OrderByDescending(p => p.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(projects?.Select(p => p.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Project>> GetProjectByIdAsync(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 project = await context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == decodeResult.Value, cancellationToken);
|
||||||
|
|
||||||
|
return project is not null
|
||||||
|
? Result.Ok(project.Map())
|
||||||
|
: Result.Fail("Failed to get specific project.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(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 activities = await context.Activities.AsNoTracking()
|
||||||
|
.Where(a => a.ProjectId == decodeResult.Value)
|
||||||
|
.OrderByDescending(a => a.CreatedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(activities?.Select(a => a.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (await context.Projects.AnyAsync(p => p.Name == request.Name && p.Sector == request.Sector && p.ProjectType == request.ProjectType, cancellationToken))
|
||||||
|
return Result.Fail("A similarly named project exists of the same project type and sector");
|
||||||
|
|
||||||
|
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
|
||||||
|
return Result.Fail($"Programme does not exist.");
|
||||||
|
|
||||||
|
context.Projects.Add(new Domain.Entities.Project
|
||||||
|
{
|
||||||
|
ProgrammeId = request.ProgrammeId,
|
||||||
|
Name = request.Name,
|
||||||
|
Description = request.Description,
|
||||||
|
Sector = request.Sector,
|
||||||
|
ProjectType = request.ProjectType,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create project");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
||||||
|
return Result.Fail($"Project does not exist.");
|
||||||
|
|
||||||
|
if (request.MilestoneId.HasValue)
|
||||||
|
if (!await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken))
|
||||||
|
return Result.Fail($"Milestone does not exist.");
|
||||||
|
|
||||||
|
context.Activities.Add(new Domain.Entities.Activity
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
MilestoneId = request.MilestoneId,
|
||||||
|
Category = request.Category,
|
||||||
|
Amount = request.Amount,
|
||||||
|
Title = request.Title,
|
||||||
|
Description = request.Description,
|
||||||
|
PerformedAt = request.PerformedAt,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create activity.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Projects;
|
||||||
|
|
||||||
|
public record UpdateProjectStatusRequest(long ProjectId, ApprovalStatus Status);
|
||||||
|
|
||||||
|
public record CreateProjectRequest(long ProgrammeId, string Name, string Description, IndustrySector Sector, ProjectType ProjectType);
|
||||||
|
|
||||||
|
public record CreateActivityRequest(long ProjectId, long? MilestoneId, string Category, decimal Amount, string Title, string Description, DateTime? PerformedAt);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using static PostFundManagement.Domain.Extensions.Constants;
|
using static PostFundManagement.Domain.Extensions.Constants;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using static PostFundManagement.Domain.Extensions.Constants;
|
using static PostFundManagement.Domain.Extensions.Constants;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using static PostFundManagement.Domain.Extensions.Constants;
|
using static PostFundManagement.Domain.Extensions.Constants;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Shared;
|
||||||
|
|
||||||
|
public sealed class IdentityService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
|
||||||
|
{
|
||||||
|
public async ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
return !await context.Users.AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken)
|
||||||
|
? Result.Fail("Invalid user")
|
||||||
|
: Result.Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api.Configuration;
|
using PostFundManagement.Domain.Api.Configuration;
|
||||||
using PostFundManagement.Domain.Api.Models;
|
using PostFundManagement.Domain.Api.Models;
|
||||||
using PostFundManagement.Domain.Sdk;
|
using PostFundManagement.Domain.Sdk;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Users;
|
||||||
|
|
||||||
|
public record UserFilterRequest(ActivityStatus? Status = null, string? Email = null, string? SortBy = null, bool SortDescending = false);
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Users;
|
||||||
|
|
||||||
|
public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
|
||||||
|
{
|
||||||
|
public async ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
context.Users.Add(new Domain.Entities.User
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
Email = email,
|
||||||
|
Status = ActivityStatus.Active,
|
||||||
|
LastLoginAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user!.Email = email;
|
||||||
|
user.LastLoginAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
context.Users.Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to sync user");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var query = context.Users.AsNoTracking();
|
||||||
|
|
||||||
|
if (request.Status.HasValue)
|
||||||
|
query = query.Where(u => u.Status == request.Status.Value);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.Email))
|
||||||
|
query = query.Where(u => u.Email != null && u.Email.Contains(request.Email));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.SortBy))
|
||||||
|
query = request.SortBy.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"email" => request.SortDescending ? query.OrderByDescending(u => u.Email) : query.OrderBy(u => u.Email),
|
||||||
|
"lastloginat" => request.SortDescending ? query.OrderByDescending(u => u.LastLoginAt) : query.OrderBy(u => u.LastLoginAt),
|
||||||
|
"status" => request.SortDescending ? query.OrderByDescending(u => u.Status) : query.OrderBy(u => u.Status),
|
||||||
|
_ => request.SortDescending ? query.OrderByDescending(u => u.Id) : query.OrderBy(u => u.Id)
|
||||||
|
};
|
||||||
|
else
|
||||||
|
query = query.OrderBy(u => u.Id);
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var users = await query.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize).ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(users?.Select(u => u.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using PostFundManagement.Domain.Abstractions;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Events.Communications;
|
|
||||||
|
|
||||||
public class SyncUserEvent : EventBase, IEvent
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = nameof(SyncUserEvent);
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,13 @@ namespace PostFundManagement.Domain.Extensions;
|
|||||||
|
|
||||||
public static class Constants
|
public static class Constants
|
||||||
{
|
{
|
||||||
|
public const string EmaiItemKey = "UserEmail";
|
||||||
|
|
||||||
|
public const string UserIdItemKey = "UserId";
|
||||||
|
|
||||||
public const string DefaultSchedulerName = "pfm-scheduler";
|
public const string DefaultSchedulerName = "pfm-scheduler";
|
||||||
|
|
||||||
public const string GeneralS3SettingsSection = "PfmS3Settings";
|
public const string GeneralS3SettingsSection = "GeneralS3Settings";
|
||||||
|
|
||||||
public const string EvidenceS3SettingsSection = "EvidenceS3Settings";
|
public const string EvidenceS3SettingsSection = "EvidenceS3Settings";
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,32 @@
|
|||||||
|
|
||||||
public static class General
|
public static class General
|
||||||
{
|
{
|
||||||
public static ProblemDetails ToProblems(this IReadOnlyList<IError> errors, string title) => new()
|
public static Guid GetUserId(this HttpContext context) => (Guid)context.Items[Constants.UserIdItemKey]!;
|
||||||
|
|
||||||
|
public static string GetUserEmail(this HttpContext context) => (string)context.Items[Constants.EmaiItemKey]!;
|
||||||
|
|
||||||
|
public static (bool Decoded, Guid userId) GetUserId(this ClaimsPrincipal principal)
|
||||||
{
|
{
|
||||||
Errors = errors.Select(e => new KeyValuePair<string, string[]>(e.Message,
|
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
||||||
[.. e.Reasons.Select(r => r.Message)])).ToDictionary(),
|
|
||||||
Detail = errors.FirstOrDefault()?.Message,
|
var decoded = Guid.TryParse(sidClaim, out var userId);
|
||||||
Status = StatusCodes.Status400BadRequest,
|
|
||||||
Title = title,
|
return (decoded, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (bool Decoded, Guid email) GetUserEmail(this ClaimsPrincipal principal)
|
||||||
|
{
|
||||||
|
var emailClaim = principal.FindFirst("email")?.Value ?? principal.FindFirst(ClaimTypes.Email)?.Value;
|
||||||
|
|
||||||
|
var decoded = Guid.TryParse(emailClaim, out var email);
|
||||||
|
|
||||||
|
return (decoded, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Microsoft.AspNetCore.Mvc.ProblemDetails ToProblems(this IReadOnlyList<IError> errors) => new()
|
||||||
|
{
|
||||||
|
Detail = string.Join(',', errors.Select(e => e.Message)),
|
||||||
|
Status = StatusCodes.Status400BadRequest,
|
||||||
|
Title = errors[0]?.Message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
<PackageReference Include="Polly" Version="8.7.0" />
|
<PackageReference Include="Polly" Version="8.7.0" />
|
||||||
<PackageReference Include="Polly.Extensions" Version="8.7.0" />
|
<PackageReference Include="Polly.Extensions" Version="8.7.0" />
|
||||||
|
|
||||||
|
<Using Include="System.Security.Claims" />
|
||||||
<Using Include="AccessTokenClient" />
|
<Using Include="AccessTokenClient" />
|
||||||
<Using Include="Microsoft.AspNetCore.Authentication" />
|
<Using Include="Microsoft.AspNetCore.Authentication" />
|
||||||
<Using Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
<Using Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
||||||
|
|||||||
Reference in New Issue
Block a user