Endpoints #2
@@ -0,0 +1,35 @@
|
|||||||
|
using PostFundManagement.Application.Communications;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using static PostFundManagement.Domain.Extensions.General;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Communications;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetInvitesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/communications/invites/{page:int}/{pageSize:int}", async (CommunicationsService service,
|
||||||
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetInvitesAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of sent invitations")
|
||||||
|
.WithName(typeof(GetInvitesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Invite[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Communications)
|
||||||
|
.CacheOutput();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Communications;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Communications;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class SendInviteEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/communications/invite", async (InviteRequest request,
|
||||||
|
HttpContext context, CommunicationsService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.SendInviteAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
|
||||||
|
.WithName(typeof(SendInviteEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Invite>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Communications);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using PostFundManagement.Application.Communications.Events;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Communications;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class SyncIdentityUserEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/integration/sync", async (IJobOrchestrator jobOrchestrator,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var syncEvent = new SyncUserEvent();
|
||||||
|
|
||||||
|
await jobOrchestrator.SendAsync(syncEvent, cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
})
|
||||||
|
.WithDescription("Trigger integration and data synchronization in the background")
|
||||||
|
.WithName(typeof(SyncIdentityUserEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Communications);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateIndicatorEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/evidence/indicators", async (CreateIndicatorRequest request, HttpContext context,
|
||||||
|
EvidenceService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateIndicatorAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Define indicators and baselines/targets (M08)")
|
||||||
|
.WithName(typeof(CreateIndicatorEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateMilestoneEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/evidence/milestones", async (CreateMilestoneRequest request, HttpContext context,
|
||||||
|
EvidenceService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateMilestoneAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Define contractual milestones (M07)")
|
||||||
|
.WithName(typeof(CreateMilestoneRequest).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetEvidenceFilesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetEvidenceFilesAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of all uploaded evidence files metadata")
|
||||||
|
.WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Models.Evidence[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence)
|
||||||
|
.CacheOutput();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetIndicatorsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetIndicatorsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of indicators")
|
||||||
|
.WithName(typeof(GetIndicatorsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Indicator[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetMilestonesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/evidence/milestones/{page:int}/{pageSize:int}", async (EvidenceService service,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetMilestonesAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of all milestones")
|
||||||
|
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Milestone[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetProjectMilestonesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/evidence/milestones/project/{projectId}", async (string projectId,
|
||||||
|
EvidenceService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetMilestonesByProjectIdAsync(projectId, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of milestones for a project")
|
||||||
|
.WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Milestone[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class RecordIndicatorAmountEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/evidence/indicators/{indicatorId}/actuals/{actualAmount:decimal}", async (string indicatorId, decimal actualAmount,
|
||||||
|
HttpContext context, EvidenceService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.RecordIndicatorActualAmountAsync(indicatorId, actualAmount, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Record actual values for indicators (M08)")
|
||||||
|
.WithName(typeof(RecordIndicatorAmountEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using PostFundManagement.Application.Evidence;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class UploadEvidenceFilesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/evidence/files", async (HttpRequest request, EvidenceService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var userId = request.HttpContext.GetUserId();
|
||||||
|
|
||||||
|
if (!request.HasFormContentType)
|
||||||
|
return Results.BadRequest("Request must be a multipart form.");
|
||||||
|
|
||||||
|
var form = await request.ReadFormAsync(cancellationToken);
|
||||||
|
var file = form.Files.GetFile("file");
|
||||||
|
|
||||||
|
if (file == null || file.Length == 0)
|
||||||
|
return Results.BadRequest("No file uploaded or file is empty.");
|
||||||
|
|
||||||
|
var result = await service.UploadEvidenceFileAsync(userId, form, file, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.DisableAntiforgery()
|
||||||
|
.WithDescription("Upload supporting evidence file to S3 and register metadata (M14)")
|
||||||
|
.WithName(typeof(UploadEvidenceFilesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class ApproveDisbursementEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/execution/disbursements/{disbursementId}/approve", async (string disbursementId,
|
||||||
|
ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.ApproveDisbursementAsync(disbursementId, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Approve disbursement and release payment (M06)")
|
||||||
|
.WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateDisbursementEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/execution/disbursements", async (CreateDisbursementRequest request,
|
||||||
|
ExecutionService service, HttpContext context, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateDisbursementAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Create a new disbursement schedule (M06)")
|
||||||
|
.WithName(typeof(CreateDisbursementEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateRiskEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/execution/risks", async (CreateRiskRequest request, ExecutionService service,
|
||||||
|
HttpContext context, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateRiskAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Add risk to risk register (M09)")
|
||||||
|
.WithName(typeof(CreateRiskEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetDisbursementsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/execution/disbursements", async (ExecutionService service,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetDisbursementsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of disbursements")
|
||||||
|
.WithName(typeof(GetDisbursementsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Disbursement[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetProjectRisksEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/execution/risks/project/{projectId}", async (string projectId,
|
||||||
|
ExecutionService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetProjectRisksAsync(projectId, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of risks for a project")
|
||||||
|
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Risk[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Execution;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Execution;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetRisksEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/execution/risks", async (ExecutionService service,
|
||||||
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetRisksAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of all risks")
|
||||||
|
.WithName(typeof(GetRisksEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateOrganisationEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/governance/organisations", async (CreateOrganisationRequest request, HttpContext context,
|
||||||
|
GovernanceService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateOrganisationAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Create a new organization profile")
|
||||||
|
.WithName(typeof(CreateOrganisationEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class DashboardEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/dashboard", async (GovernanceService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetDashboardAsync(cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get executive portfolio dashboard metrics and KPIs")
|
||||||
|
.WithName(typeof(DashboardEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Dashboard>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetInstrumentsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/instruments", async (Application.Governance.GovernanceService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetInstrumentsAsync(cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get all funding instruments")
|
||||||
|
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Instrument[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetOrganisationByIdEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/organisations/{organisationId}", async (string organisationId,
|
||||||
|
GovernanceService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetOrganisationByIdAsync(organisationId, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get organization profile by ID")
|
||||||
|
.WithName(typeof(GetOrganisationByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Organisation>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetOrganisationsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/organisations", async (GovernanceService service,
|
||||||
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetOrganisationsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get a paginated directory of organizations")
|
||||||
|
.WithName(typeof(GetOrganisationsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Organisation[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetPortfoliosEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/portfolios", async (GovernanceService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetPorfoliosAsync(cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get all portfolios")
|
||||||
|
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Portfolio[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Governance;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Governance;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetProgrammesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/programmes", async (GovernanceService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetProgrammesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get all programmes")
|
||||||
|
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Programme[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateAwardEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/grants/awards", async (CreateAwardRequest request, HttpContext context,
|
||||||
|
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateAwardAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Register an approved funding award (M01)")
|
||||||
|
.WithName(typeof(CreateAwardEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateBudgetEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/grants/budgets", async (CreateBudgetRequest request, HttpContext context,
|
||||||
|
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateBudgetAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Create a new budget line (M05)")
|
||||||
|
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateContractEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/grants/contracts", async (CreateContractRequest request, HttpContext context,
|
||||||
|
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateContractAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Create a contract record from an approved award (M02)")
|
||||||
|
.WithName(typeof(CreateContractEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class ExecuteContractEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/grants/contracts/execute", async (ExecuteContractRequest request, HttpContext context,
|
||||||
|
GrantsService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.ExecuteContractAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Execute the contract and lock the contractual version")
|
||||||
|
.WithName(typeof(ExecuteContractEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetAwardByIdEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/grants/awards/{awardId}", async (string awardId, GrantsService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetAwardByIdAsync(awardId, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get award by ID")
|
||||||
|
.WithName(typeof(GetAwardByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Award>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetAwardsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/grants/awards", async (GrantsService service,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetAwardsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of awards")
|
||||||
|
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Award[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetBudgetByIdEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/grants/budgets/{budgetId}", async (string budgetId, GrantsService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetBudgetByIdAsync(budgetId, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get budget by ID")
|
||||||
|
.WithName(typeof(GetBudgetByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Budget>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetBudgetsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/grants/budgets", async (GrantsService service,
|
||||||
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetBudgetsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get all budgets")
|
||||||
|
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Budget[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetContractByIdEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/grants/contracts/{contractId}", async (string contractId, GrantsService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetContractByIdAsync(contractId, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get contract by ID")
|
||||||
|
.WithName(typeof(GetContractByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Contract>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Grants;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Grants;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetContractsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/grants/contracts", async (GrantsService service,
|
||||||
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetContractsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get all contracts")
|
||||||
|
.WithName(typeof(GetContractsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Contract[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateActivityEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/projects/activities", async (CreateActivityRequest request,
|
||||||
|
HttpContext context, ProjectService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateActivityAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Add a new activity under project (M04)")
|
||||||
|
.WithName(typeof(CreateActivityEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateProjectEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/projects", async (CreateProjectRequest request, HttpContext context,
|
||||||
|
ProjectService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.CreateProjectAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Create a new project (M04)")
|
||||||
|
.WithName(typeof(CreateProjectEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetActivitiesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/projects/{projectId}/activities", async (string projectId,
|
||||||
|
ProjectService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetActivitiesAsync(projectId, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of activities for a project")
|
||||||
|
.WithName(typeof(GetActivitiesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Models.Activity[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetProjectByIdEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/projects/{projectId}", async (string projectId, ProjectService service,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetProjectByIdAsync(projectId, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Get project by ID")
|
||||||
|
.WithName(typeof(GetProjectByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Project[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetProjectsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/projects", async (ProjectService service,
|
||||||
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetProjectsAsync(Pagination.Create(page, pageSize), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Get a list of projects")
|
||||||
|
.WithName(typeof(GetProjectsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Project[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Projects;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Projects;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class UpdateProjectEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/projects/status", async (UpdateProjectStatusRequest request, HttpContext context,
|
||||||
|
ProjectService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Update project status (M04)")
|
||||||
|
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Users;
|
||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Users;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetUsersEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/users/{page:int}/{pageSize:int}", async (UserFilterRequest request, UserService service,
|
||||||
|
int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.GetUsersAsync(Pagination.Create(page, pageSize), request, cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok(result.Value)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
|
||||||
|
.WithName(typeof(GetUsersEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<User[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Users);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Application.Users;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Users;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class SyncUserEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/users/sync", async (HttpContext context,
|
||||||
|
UserService service, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var result = await service.SyncUserAsync(context.GetUserId(), context.GetUserEmail(), cancellationToken);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? Results.Ok()
|
||||||
|
: Results.BadRequest(result.Errors.ToProblems());
|
||||||
|
})
|
||||||
|
.WithDescription("Synchronise caller user profile using id_token's SID, inserting/updating user details in database")
|
||||||
|
.WithName(typeof(SyncUserEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces<Microsoft.AspNetCore.Mvc.ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.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;
|
|
||||||
using PostFundManagement.Infrastructure.Database;
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Extensions;
|
namespace PostFundManagement.Api.Extensions;
|
||||||
@@ -54,18 +54,18 @@ public static class Security
|
|||||||
|
|
||||||
services.AddDataProtection().PersistKeysToDbContext<DataProtectionDbContext>()
|
services.AddDataProtection().PersistKeysToDbContext<DataProtectionDbContext>()
|
||||||
.ProtectKeysWithCertificate(certificate)
|
.ProtectKeysWithCertificate(certificate)
|
||||||
.SetApplicationName("LiteCharmsApp");
|
.SetApplicationName("PfmApp");
|
||||||
|
|
||||||
services.Configure<DataProtectionOptions>(options => options.ApplicationDiscriminator = "LiteCharmsApp");
|
services.Configure<DataProtectionOptions>(options => options.ApplicationDiscriminator = "PfmApp");
|
||||||
|
|
||||||
services.ConfigureCookieOidcSameSiteSupport();
|
services.ConfigureCookieOidcSameSiteSupport();
|
||||||
|
|
||||||
var configSection = configuration.GetSection(nameof(SecuritySettings));
|
var configSection = configuration.GetSection(nameof(SecurityClientSettings));
|
||||||
|
|
||||||
var authOptions = new SecuritySettings();
|
var authOptions = new SecurityClientSettings();
|
||||||
configSection.Bind(authOptions);
|
configSection.Bind(authOptions);
|
||||||
|
|
||||||
services.Configure<SecuritySettings>(configSection);
|
services.Configure<SecurityClientSettings>(configSection);
|
||||||
|
|
||||||
services.AddAuthentication(options =>
|
services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
@@ -76,7 +76,7 @@ public static class Security
|
|||||||
{
|
{
|
||||||
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||||
options.Cookie.SameSite = SameSiteMode.Lax;
|
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||||
options.Cookie.Name = "LiteCharmsApp.Session";
|
options.Cookie.Name = "PfmApp.Session";
|
||||||
})
|
})
|
||||||
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
|
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
|
||||||
{
|
{
|
||||||
@@ -123,4 +123,31 @@ public static class Security
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddApiSecurity(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var configSection = configuration.GetSection(nameof(SecuritySettings));
|
||||||
|
|
||||||
|
var authOptions = new SecuritySettings();
|
||||||
|
configSection.Bind(authOptions);
|
||||||
|
|
||||||
|
services.Configure<SecuritySettings>(configSection);
|
||||||
|
|
||||||
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.Authority = authOptions.Authority;
|
||||||
|
options.Audience = authOptions.Audience;
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidIssuer = authOptions.Authority,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateIssuer = true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddAuthorization();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,9 @@
|
|||||||
<Using Include="Microsoft.AspNetCore.Authentication" />
|
<Using Include="Microsoft.AspNetCore.Authentication" />
|
||||||
<Using Include="Microsoft.AspNetCore.Authentication.Cookies" />
|
<Using Include="Microsoft.AspNetCore.Authentication.Cookies" />
|
||||||
<Using Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
<Using Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
|
<Using Include="Microsoft.IdentityModel.Tokens" />
|
||||||
|
<Using Include="System.Security.Claims" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Health Checks -->
|
<!-- Health Checks -->
|
||||||
@@ -90,8 +93,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\PostFundManagement.Domain\PostFundManagement.Domain.csproj" />
|
<ProjectReference Include="..\PostFundManagement.Application\PostFundManagement.Application.csproj" />
|
||||||
<ProjectReference Include="..\PostFundManagement.Infrastructure\PostFundManagement.Infrastructure.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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,23 +12,25 @@ 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);
|
||||||
|
|
||||||
builder.Services.AddMediator();
|
builder.Services.AddMediator();
|
||||||
builder.Services.AddSecurityApiSdk(builder.Configuration);
|
builder.Services.AddSecurityApiSdk(builder.Configuration);
|
||||||
builder.Services.AddWebSecurity(builder.Configuration);
|
builder.Services.AddApiSecurity(builder.Configuration);
|
||||||
|
|
||||||
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>));
|
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>));
|
||||||
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>));
|
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>));
|
||||||
|
|
||||||
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.AddGarageS3(builder.Configuration);
|
||||||
|
builder.Services.AddRegisterServices();
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddHashServices(builder.Configuration);
|
|
||||||
builder.Services.AddDataProtectionDatabase(builder.Configuration);
|
builder.Services.AddDataProtectionDatabase(builder.Configuration);
|
||||||
builder.Services.AddApplicationDbContext(builder.Configuration);
|
builder.Services.AddApplicationDbContext(builder.Configuration);
|
||||||
|
|
||||||
@@ -58,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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Domain.Models;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Communications;
|
||||||
|
|
||||||
|
public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory) : IService
|
||||||
|
{
|
||||||
|
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var invite = new Domain.Entities.Invite
|
||||||
|
{
|
||||||
|
OrganisationId = request.OrganisationId,
|
||||||
|
InvitedOrganisationId = request.InvitedOrganisationId,
|
||||||
|
AwardId = request.AwardId,
|
||||||
|
ContractId = request.ContractId,
|
||||||
|
Recipient = request.Recipient,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Invites.Add(invite);
|
||||||
|
|
||||||
|
context.Notifications.Add(new Domain.Entities.Notification
|
||||||
|
{
|
||||||
|
OrganisationId = request.OrganisationId,
|
||||||
|
Recipient = request.Recipient,
|
||||||
|
Platform = request.Platform,
|
||||||
|
Status = NotificationStatus.Pending,
|
||||||
|
Subject = "Invitation to Onboard",
|
||||||
|
Message = request.Message,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok(invite.Map())
|
||||||
|
: Result.Fail("Failed to complete the insite distribution");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var invites = await context.Invites.AsNoTracking()
|
||||||
|
.OrderByDescending(i => i.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(invites?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PostFundManagement.Application.Communications.Events;
|
||||||
|
|
||||||
|
public class SyncUserEventHandler(ILogger<SyncUserEvent> logger) : INotificationHandler<SyncUserEvent>
|
||||||
|
{
|
||||||
|
public ValueTask Handle(SyncUserEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Integration Sync executed. Correlation ID: {CorrelationId}", notification.CorrelationId);
|
||||||
|
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Communications;
|
||||||
|
|
||||||
|
public record InviteRequest(long OrganisationId, long InvitedOrganisationId, long AwardId, long ContractId, Guid Recipient, string Message, NotificationPlatform Platform);
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
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.Evidence;
|
||||||
|
|
||||||
|
public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
|
||||||
|
[FromKeyedServices(Constants.EvidenceBucketName)] IS3Service s3Service) : IService
|
||||||
|
{
|
||||||
|
public async ValueTask<Result> UploadEvidenceFileAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = long.TryParse(form["projectId"], out var projectId);
|
||||||
|
_ = long.TryParse(form["milestoneId"], out var milestoneId);
|
||||||
|
_ = long.TryParse(form["indicatorId"], out var indicatorId);
|
||||||
|
_ = long.TryParse(form["activityId"], out var activityId);
|
||||||
|
_ = Enum.TryParse<EvidenceType>(form["type"], out var type);
|
||||||
|
|
||||||
|
using var stream = file.OpenReadStream();
|
||||||
|
|
||||||
|
var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken);
|
||||||
|
|
||||||
|
if (uploadResult.IsFailed)
|
||||||
|
return Result.Fail(uploadResult.Errors.FirstOrDefault()?.Message ?? "File upload failed.");
|
||||||
|
|
||||||
|
var documentUrl = uploadResult.Value;
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var evidence = new Domain.Entities.Evidence
|
||||||
|
{
|
||||||
|
ProjectId = projectId,
|
||||||
|
MilestoneId = milestoneId,
|
||||||
|
IndicatorId = indicatorId,
|
||||||
|
ActivityId = activityId,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Version = 1,
|
||||||
|
Type = type,
|
||||||
|
DocumentUrl = documentUrl,
|
||||||
|
Status = ApprovalStatus.Pending
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Evidences.Add(evidence);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Evidence upload failed");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash);
|
||||||
|
|
||||||
|
if (decodeResult.IsFailed)
|
||||||
|
return Result.Fail(decodeResult.Errors);
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var totalUpdated = await context.Indicators.Where(i => i.Id == decodeResult.Value)
|
||||||
|
.ExecuteUpdateAsync(f => f
|
||||||
|
.SetProperty(f => f.ActualAmount, amount)
|
||||||
|
.SetProperty(f => f.CreatedBy, userId), cancellationToken);
|
||||||
|
|
||||||
|
return totalUpdated > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to record actual amount");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Milestone[]>> GetMilestonesByProjectIdAsync(string projectIdHash, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
|
||||||
|
|
||||||
|
if (decodeResult.IsFailed)
|
||||||
|
return Result.Fail(decodeResult.Errors);
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var milestones = await context.Milestones.AsNoTracking()
|
||||||
|
.Where(m => m.ProjectId == decodeResult.Value)
|
||||||
|
.OrderBy(m => m.DueAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var milestones = await context.Milestones.AsNoTracking()
|
||||||
|
.OrderByDescending(m => m.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var indicators = await context.Indicators.AsNoTracking()
|
||||||
|
.OrderByDescending(i => i.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(indicators?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var evidence = await context.Evidences.AsNoTracking()
|
||||||
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
|
.Skip((pagination.Page - 1) * pagination.PageSize)
|
||||||
|
.Take(pagination.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Result.Ok(evidence?.Select(i => i.Map()).ToArray() ?? []);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest 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.Milestones.Add(new Domain.Entities.Milestone
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
DueAt = request.DueAt,
|
||||||
|
Name = request.Name,
|
||||||
|
Priority = request.Priority,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create the milestone");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
return Result.Fail($"Project with ID {request.ProjectId} does not exist.");
|
||||||
|
|
||||||
|
context.Indicators.Add(new Domain.Entities.Indicator
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
Name = request.Name,
|
||||||
|
UnitOfMeasure = request.UnitOfMeasure,
|
||||||
|
BaselineAmount = request.BaselineAmount,
|
||||||
|
TargetAmount = request.TargetAmount,
|
||||||
|
ActualAmount = request.BaselineAmount,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create the invite");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Evidence;
|
||||||
|
|
||||||
|
public record RecordActualRequest(decimal ActualAmount);
|
||||||
|
|
||||||
|
public record CreateMilestoneRequest(long ProjectId, DateTime DueAt, string Name, Priority Priority);
|
||||||
|
|
||||||
|
public record CreateIndicatorRequest(long ProjectId, string Name, UnitOfMeasure UnitOfMeasure, decimal BaselineAmount, decimal TargetAmount);
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
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);
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyOriginatorKeyFile>..\PostFundManagement.snk</AssemblyOriginatorKeyFile>
|
||||||
|
<SignAssembly>True</SignAssembly>
|
||||||
|
<PackageLicenseFile>..\LICENSE</PackageLicenseFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PostFundManagement.Domain\PostFundManagement.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\PostFundManagement.Infrastructure\PostFundManagement.Infrastructure.csproj" />
|
||||||
|
</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 -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Asp.Versioning" />
|
||||||
|
<Using Include="System.Security.Claims" />
|
||||||
|
<Using Include="System.Diagnostics" />
|
||||||
|
<Using Include="MimeKit" />
|
||||||
|
<Using Include="MailKit.Net.Smtp" />
|
||||||
|
<Using Include="Quartz" />
|
||||||
|
<Using Include="Refit" />
|
||||||
|
<Using Include="AccessTokenClient" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Authentication" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Authentication.Cookies" />
|
||||||
|
<Using Include="IdentityModel.AspNetCore.OAuth2Introspection" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
|
<Using Include="Microsoft.Extensions.Configuration" />
|
||||||
|
<Using Include="Amazon.S3" />
|
||||||
|
<Using Include="Amazon.S3.Model" />
|
||||||
|
<Using Include="Amazon.Runtime" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" />
|
||||||
|
<Using Include="Microsoft.EntityFrameworkCore" />
|
||||||
|
<Using Include="Microsoft.EntityFrameworkCore.Design" />
|
||||||
|
<Using Include="Microsoft.EntityFrameworkCore.Metadata.Builders" />
|
||||||
|
<Using Include="Mediator" />
|
||||||
|
<Using Include="FluentResults" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.DataProtection" />
|
||||||
|
<Using Include="System.Security.Cryptography.X509Certificates" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage" />
|
||||||
|
<Using Include="System.Text.Json.Serialization" />
|
||||||
|
<Using Include="System.Reflection" />
|
||||||
|
<Using Include="Microsoft.Extensions.DependencyInjection.Extensions" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Routing" />
|
||||||
|
<Using Include="System.Web" />
|
||||||
|
<Using Include="Microsoft.IdentityModel.Tokens" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Http" />
|
||||||
|
<Using Include="HashidsNet" />
|
||||||
|
<Using Include="System.Net" />
|
||||||
|
<Using Include="System.Text.RegularExpressions" />
|
||||||
|
<Using Include="System.Globalization" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||||
|
<Using Include="Microsoft.Extensions.Hosting" />
|
||||||
|
<Using Include="System.Text" />
|
||||||
|
<Using Include="System.Text.Json" />
|
||||||
|
<Using Include="System.Threading.Channels" />
|
||||||
|
<Using Include="System.Collections.ObjectModel" />
|
||||||
|
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
|
<Using Include="System.Security.Cryptography" />
|
||||||
|
<Using Include="Microsoft.Extensions.Options" />
|
||||||
|
<Using Include="Microsoft.Extensions.Logging" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace PostFundManagement.Domain.Services;
|
namespace PostFundManagement.Application.Shared;
|
||||||
|
|
||||||
public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
|
public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
|
||||||
{
|
{
|
||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using static PostFundManagement.Domain.Extensions.Constants;
|
using static PostFundManagement.Domain.Extensions.Constants;
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Services;
|
namespace PostFundManagement.Application.Shared;
|
||||||
|
|
||||||
public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractBucketName)] IAmazonS3 amazonS3) :
|
public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractBucketName)] IAmazonS3 amazonS3) :
|
||||||
S3ServiceBase(amazonS3), IS3Service
|
S3ServiceBase(amazonS3), IS3Service
|
||||||
+2
-3
@@ -1,10 +1,9 @@
|
|||||||
using System.Diagnostics;
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Configuration.Email;
|
using PostFundManagement.Domain.Configuration.Email;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
using PostFundManagement.Domain.Models.Email;
|
using PostFundManagement.Domain.Models.Email;
|
||||||
using static PostFundManagement.Domain.Extensions.EmailTelemetry;
|
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Services;
|
namespace PostFundManagement.Application.Shared;
|
||||||
|
|
||||||
public sealed class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
public sealed class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
||||||
{
|
{
|
||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using static PostFundManagement.Domain.Extensions.Constants;
|
using static PostFundManagement.Domain.Extensions.Constants;
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Services;
|
namespace PostFundManagement.Application.Shared;
|
||||||
|
|
||||||
public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceBucketName)] IAmazonS3 amazonS3) :
|
public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceBucketName)] IAmazonS3 amazonS3) :
|
||||||
S3ServiceBase(amazonS3), IS3Service
|
S3ServiceBase(amazonS3), IS3Service
|
||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using static PostFundManagement.Domain.Extensions.Constants;
|
using static PostFundManagement.Domain.Extensions.Constants;
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Services;
|
namespace PostFundManagement.Application.Shared;
|
||||||
|
|
||||||
public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralBucketName)] IAmazonS3 amazonS3) :
|
public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralBucketName)] IAmazonS3 amazonS3) :
|
||||||
S3ServiceBase(amazonS3), IS3Service
|
S3ServiceBase(amazonS3), IS3Service
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Services;
|
namespace PostFundManagement.Application.Shared;
|
||||||
|
|
||||||
public sealed partial class HashService(IHashids hasher) : IService
|
public sealed partial class HashService(IHashids hasher) : IService
|
||||||
{
|
{
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-5
@@ -1,18 +1,19 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
namespace PostFundManagement.Domain.Services;
|
namespace PostFundManagement.Application.Shared;
|
||||||
|
|
||||||
public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<SecurityClientSettings> clientOptions)
|
public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<SecurityClientSettings> clientOptions)
|
||||||
{
|
{
|
||||||
private readonly SecurityClientSettings clientSettings = clientOptions.Value;
|
private readonly SecurityClientSettings clientSettings = clientOptions.Value;
|
||||||
|
|
||||||
public async Task<Result<Api.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default)
|
public async Task<Result<Domain.Api.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var request = new Api.Models.TokenRequest
|
var request = new Domain.Api.Models.TokenRequest
|
||||||
{
|
{
|
||||||
ClientId = clientSettings.ClientId,
|
ClientId = clientSettings.ClientId,
|
||||||
ClientSecret = clientSettings.ClientSecret,
|
ClientSecret = clientSettings.ClientSecret,
|
||||||
@@ -29,11 +30,11 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
|
|||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var tokenResponse = JsonSerializer.Deserialize<Api.Models.TokenResponse>(contentRaw);
|
var tokenResponse = JsonSerializer.Deserialize<Domain.Api.Models.TokenResponse>(contentRaw);
|
||||||
|
|
||||||
return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken)
|
return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken)
|
||||||
? Result.Ok(tokenResponse)
|
? Result.Ok(tokenResponse)
|
||||||
: Result.Fail<Api.Models.TokenResponse>(new Error("Authentication succeeded, but no access token was found in the response payload."));
|
: Result.Fail<Domain.Api.Models.TokenResponse>(new Error("Authentication succeeded, but no access token was found in the response payload."));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
public static class EndpointTags
|
||||||
|
{
|
||||||
|
public const string Governance = nameof(Governance);
|
||||||
|
|
||||||
|
public const string Projects = nameof(Projects);
|
||||||
|
|
||||||
|
public const string Grants = nameof(Grants);
|
||||||
|
|
||||||
|
public const string Execution = nameof(Execution);
|
||||||
|
|
||||||
|
public const string Evidence = "Evidence & M&E";
|
||||||
|
|
||||||
|
public const string Communications = nameof(Communications);
|
||||||
|
|
||||||
|
public const string Users = nameof(Users);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
namespace PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
public static class General
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
||||||
|
|
||||||
|
var decoded = Guid.TryParse(sidClaim, out var userId);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
namespace PostFundManagement.Domain;
|
||||||
|
|
||||||
|
public sealed class Pagination
|
||||||
|
{
|
||||||
|
const int defaultPage = 1;
|
||||||
|
const int defaultPageSize = 100;
|
||||||
|
const long defaultIndex = 0;
|
||||||
|
|
||||||
|
public long Index { get; set; }
|
||||||
|
|
||||||
|
public int Page { get; set; }
|
||||||
|
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
public Pagination()
|
||||||
|
{
|
||||||
|
Page = defaultPage;
|
||||||
|
PageSize = defaultPageSize;
|
||||||
|
Index = defaultIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pagination(int page = defaultPage, int pageSize = defaultPageSize, long index = defaultIndex)
|
||||||
|
{
|
||||||
|
if (page < 1) page = defaultPage;
|
||||||
|
|
||||||
|
if (pageSize < 1 || pageSize > 100) pageSize = defaultPageSize;
|
||||||
|
|
||||||
|
Page = page;
|
||||||
|
PageSize = pageSize;
|
||||||
|
Index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Pagination Create(int page = defaultPage, int pageSize = defaultPageSize, long index = defaultIndex)
|
||||||
|
{
|
||||||
|
if (page < 1) page = defaultPage;
|
||||||
|
|
||||||
|
if (pageSize < 1 || pageSize > 100) pageSize = defaultPageSize;
|
||||||
|
|
||||||
|
return new(page, pageSize, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="PostFundManagement.Api/PostFundManagement.Api.csproj" />
|
<Project Path="PostFundManagement.Api/PostFundManagement.Api.csproj" />
|
||||||
|
<Project Path="PostFundManagement.Application/PostFundManagement.Application.csproj" Id="750c7754-53a7-4573-abed-5c436246a04d" />
|
||||||
<Project Path="PostFundManagement.Domain/PostFundManagement.Domain.csproj" />
|
<Project Path="PostFundManagement.Domain/PostFundManagement.Domain.csproj" />
|
||||||
<Project Path="PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj" />
|
<Project Path="PostFundManagement.Infrastructure/PostFundManagement.Infrastructure.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
Reference in New Issue
Block a user