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