Implemented basic endpoints
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Communications;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetInvitesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/communications/invites", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var query = context.Invites.AsNoTracking();
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var items = await query.OrderByDescending(i => i.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of sent invitations")
|
||||||
|
.WithName(typeof(GetInvitesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Communications);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,61 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
namespace PostFundManagement.Api.Endpoints.Communications;
|
namespace PostFundManagement.Api.Endpoints.Communications;
|
||||||
|
|
||||||
[ApiVersionTarget(1)]
|
[ApiVersionTarget(1)]
|
||||||
public class SendInviteEndpoint : IEndpoint
|
public class SendInviteEndpoint : IEndpoint
|
||||||
{
|
{
|
||||||
|
public record InviteRequest(long OrganisationId, long InvitedOrganisationId, long AwardId, long ContractId, Guid Recipient, string Message, NotificationPlatform Platform);
|
||||||
|
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapPost("api/communications/invite", () =>
|
builder.MapPost("api/communications/invite", async (InviteRequest request, ClaimsPrincipal principal,
|
||||||
|
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
||||||
|
|
||||||
|
if (!Guid.TryParse(sidClaim, out var currentUserId))
|
||||||
|
return Results.BadRequest("Missing or invalid 'sid' claim.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var invite = new Domain.Entities.Invite
|
||||||
|
{
|
||||||
|
OrganisationId = request.OrganisationId,
|
||||||
|
InvitedOrganisationId = request.InvitedOrganisationId,
|
||||||
|
AwardId = request.AwardId,
|
||||||
|
ContractId = request.ContractId,
|
||||||
|
Recipient = request.Recipient,
|
||||||
|
CreatedBy = currentUserId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Invites.Add(invite);
|
||||||
|
|
||||||
|
context.Notifications.Add(new Domain.Entities.Notification
|
||||||
|
{
|
||||||
|
OrganisationId = request.OrganisationId,
|
||||||
|
Recipient = request.Recipient,
|
||||||
|
Platform = request.Platform,
|
||||||
|
Status = NotificationStatus.Pending,
|
||||||
|
Subject = "Invitation to Onboard",
|
||||||
|
Message = request.Message,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Ok(invite)
|
||||||
|
: Results.BadRequest("Failed to create invite");
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
.RequireAuthorization()
|
||||||
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
|
.WithDescription("Trigger an invite to an organization or candidate (specifying NotificationPlatform)")
|
||||||
.WithName(typeof(SendInviteEndpoint).ToEndpointName())
|
.WithName(typeof(SendInviteEndpoint).ToEndpointName())
|
||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces<Domain.Entities.Invite>(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
.WithTags(EndpointTags.Communications);
|
.WithTags(EndpointTags.Communications);
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Communications;
|
||||||
|
|
||||||
|
public class SyncEvent : EventBase, IEvent
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = nameof(SyncEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SyncEventHandler : INotificationHandler<SyncEvent>
|
||||||
|
{
|
||||||
|
public ValueTask Handle(SyncEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"Integration Sync executed. Correlation ID: {notification.CorrelationId}");
|
||||||
|
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class SyncEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/integration/sync", async (IJobOrchestrator jobOrchestrator,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
var syncEvent = new SyncEvent();
|
||||||
|
|
||||||
|
await jobOrchestrator.SendAsync(syncEvent, cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(new { Queued = true, syncEvent.CorrelationId });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Trigger integration and data synchronization in the background")
|
||||||
|
.WithName(typeof(SyncEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Communications);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateIndicatorEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public record CreateIndicatorRequest(long ProjectId, string Name, UnitOfMeasure UnitOfMeasure, decimal BaselineAmount, decimal TargetAmount);
|
||||||
|
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/evidence/indicators", async (CreateIndicatorRequest request, ClaimsPrincipal principal,
|
||||||
|
IDbContextFactory<ApplicationDbContext> contextFactory, 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.");
|
||||||
|
|
||||||
|
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 indicator = new Domain.Entities.Indicator
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
Name = request.Name,
|
||||||
|
UnitOfMeasure = request.UnitOfMeasure,
|
||||||
|
BaselineAmount = request.BaselineAmount,
|
||||||
|
TargetAmount = request.TargetAmount,
|
||||||
|
ActualAmount = request.BaselineAmount,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Indicators.Add(indicator);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/evidence/indicators/{indicator.Id}", indicator)
|
||||||
|
: Results.BadRequest("Failed to create indicator");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Define indicators and baselines/targets (M08)")
|
||||||
|
.WithName(typeof(CreateIndicatorEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<PostFundManagement.Domain.Entities.Indicator>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class CreateMilestoneEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public record CreateMilestoneRequest(long ProjectId, DateTime DueAt, string Name, Priority Priority);
|
||||||
|
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/evidence/milestones", async (CreateMilestoneRequest request, ClaimsPrincipal principal,
|
||||||
|
IDbContextFactory<ApplicationDbContext> contextFactory, 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.");
|
||||||
|
|
||||||
|
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 milestone = new Domain.Entities.Milestone
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
DueAt = request.DueAt,
|
||||||
|
Name = request.Name,
|
||||||
|
Priority = request.Priority,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Milestones.Add(milestone);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/evidence/milestones/{milestone.Id}", milestone)
|
||||||
|
: Results.BadRequest("Failed to create a new milestone");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Define contractual milestones (M07)")
|
||||||
|
.WithName(typeof(CreateMilestoneRequest).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Milestone>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetEvidenceFilesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/evidence/files", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var query = context.Evidences.AsNoTracking();
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var items = await query.OrderByDescending(e => e.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of all uploaded evidence files metadata")
|
||||||
|
.WithName(typeof(GetEvidenceFilesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetIndicatorsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public record RecordActualRequest(decimal ActualAmount);
|
||||||
|
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/evidence/indicators", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var query = context.Indicators.AsNoTracking();
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var items = await query.OrderByDescending(i => i.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of indicators")
|
||||||
|
.WithName(typeof(GetIndicatorsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetMilestonesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/evidence/milestones", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var query = context.Milestones.AsNoTracking();
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var items = await query.OrderByDescending(m => m.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(new { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of all milestones")
|
||||||
|
.WithName(typeof(GetMilestonesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class GetProjectMilestonesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/evidence/milestones/project/{projectId:long}", async (long projectId,
|
||||||
|
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var milestones = await context.Milestones.AsNoTracking()
|
||||||
|
.Where(m => m.ProjectId == projectId)
|
||||||
|
.OrderBy(m => m.DueAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(milestones);
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of milestones for a project")
|
||||||
|
.WithName(typeof(GetProjectMilestonesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class RecordIndicatorActualAmountEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/evidence/indicators/{id:long}/actuals/{actualAmount:decimal}", async (long id, decimal actualAmount,
|
||||||
|
ClaimsPrincipal principal, IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var indicator = await context.Indicators.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
|
||||||
|
|
||||||
|
if (indicator is null)
|
||||||
|
return Results.NotFound($"Indicator with ID {id} does not exist.");
|
||||||
|
|
||||||
|
indicator.ActualAmount = actualAmount;
|
||||||
|
|
||||||
|
context.Indicators.Update(indicator);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Ok(indicator)
|
||||||
|
: Results.BadRequest("Failed to record actual amount");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Record actual values for indicators (M08)")
|
||||||
|
.WithName(typeof(RecordIndicatorActualAmountEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Indicator>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Evidence;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class UploadEvidenceFilesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/evidence/files", async (HttpRequest request, ClaimsPrincipal principal, IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
[FromKeyedServices(Constants.EvidenceS3SettingsSection)] IS3Service s3Service, 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.");
|
||||||
|
|
||||||
|
if (!request.HasFormContentType)
|
||||||
|
return Results.BadRequest("Request must be a multipart form.");
|
||||||
|
|
||||||
|
var form = await request.ReadFormAsync(cancellationToken);
|
||||||
|
var file = form.Files.GetFile("file");
|
||||||
|
|
||||||
|
if (file == null || file.Length == 0)
|
||||||
|
return Results.BadRequest("No file uploaded or file is empty.");
|
||||||
|
|
||||||
|
_ = long.TryParse(form["projectId"], out var projectId);
|
||||||
|
_ = long.TryParse(form["milestoneId"], out var milestoneId);
|
||||||
|
_ = long.TryParse(form["indicatorId"], out var indicatorId);
|
||||||
|
_ = long.TryParse(form["activityId"], out var activityId);
|
||||||
|
_ = Enum.TryParse<EvidenceType>(form["type"], out var type);
|
||||||
|
|
||||||
|
using var stream = file.OpenReadStream();
|
||||||
|
|
||||||
|
var uploadResult = await s3Service.UploadFileAsync(file.FileName, stream, file.ContentType, cancellationToken);
|
||||||
|
|
||||||
|
if (uploadResult.IsFailed)
|
||||||
|
return Results.BadRequest(uploadResult.Errors.FirstOrDefault()?.Message ?? "File upload failed.");
|
||||||
|
|
||||||
|
var documentUrl = uploadResult.Value;
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var evidence = new Domain.Entities.Evidence
|
||||||
|
{
|
||||||
|
ProjectId = projectId,
|
||||||
|
MilestoneId = milestoneId,
|
||||||
|
IndicatorId = indicatorId,
|
||||||
|
ActivityId = activityId,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Version = 1,
|
||||||
|
Type = type,
|
||||||
|
DocumentUrl = documentUrl,
|
||||||
|
Status = ApprovalStatus.Pending
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Evidences.Add(evidence);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/evidence/files/{evidence.Id}", evidence)
|
||||||
|
: Results.BadRequest("Evidence upload failed");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.DisableAntiforgery()
|
||||||
|
.WithDescription("Upload supporting evidence file to S3 and register metadata (M14)")
|
||||||
|
.WithName(typeof(UploadEvidenceFilesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<PostFundManagement.Domain.Entities.Evidence>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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 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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var disbursement = await context.Disbursements.FirstOrDefaultAsync(d => d.Id == id, cancellationToken);
|
||||||
|
|
||||||
|
if (disbursement == null)
|
||||||
|
return Results.NotFound($"Disbursement with ID {id} does not exist.");
|
||||||
|
|
||||||
|
disbursement.Status = ApprovalStatus.Approved;
|
||||||
|
disbursement.UpdatedBy = userId;
|
||||||
|
disbursement.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
context.Disbursements.Update(disbursement);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Ok(disbursement)
|
||||||
|
: Results.BadRequest("Failed to approve payment");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Approve disbursement and release payment (M06)")
|
||||||
|
.WithName(typeof(ApproveDisbursementEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Disbursement>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
if (request.Amount <= 0)
|
||||||
|
return Results.BadRequest("Amount must be greater than zero.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
||||||
|
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
|
||||||
|
|
||||||
|
var disbursement = new Domain.Entities.Disbursement
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
MilestoneId = request.MilestoneId,
|
||||||
|
Amount = request.Amount,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Disbursements.Add(disbursement);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/execution/disbursements/{disbursement.Id}", disbursement)
|
||||||
|
: Results.BadRequest("Failed to create disbursement");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Create a new disbursement schedule (M06)")
|
||||||
|
.WithName(typeof(CreateDisbursementEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Disbursement>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
||||||
|
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
|
||||||
|
|
||||||
|
var risk = new Domain.Entities.Risk
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
Likelihood = request.Likelihood,
|
||||||
|
Impact = request.Impact,
|
||||||
|
Name = request.Name,
|
||||||
|
Description = request.Description,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Risks.Add(risk);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/execution/risks/{risk.Id}", risk)
|
||||||
|
: Results.BadRequest("Failed to create project risk");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Add risk to risk register (M09)")
|
||||||
|
.WithName(typeof(CreateRiskEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Risk>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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 GetDisbursementsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/execution/disbursements", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
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 });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of disbursements")
|
||||||
|
.WithName(typeof(GetDisbursementsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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 GetProjectRisksEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/execution/risks/project/{projectId:long}", async (long projectId,
|
||||||
|
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var risks = await context.Risks.AsNoTracking()
|
||||||
|
.Where(r => r.ProjectId == projectId)
|
||||||
|
.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(risks);
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of risks for a project")
|
||||||
|
.WithName(typeof(GetProjectRisksEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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 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) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
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 });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of all risks")
|
||||||
|
.WithName(typeof(GetRisksEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Execution);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var organisation = new Domain.Entities.Organisation
|
||||||
|
{
|
||||||
|
Name = request.Name,
|
||||||
|
RegistrationNo = request.RegistrationNo,
|
||||||
|
Email = request.Email,
|
||||||
|
Type = request.Type,
|
||||||
|
Status = ActivityStatus.Active,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Organisations.Add(organisation);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/governance/organisations/{organisation.Id}", organisation)
|
||||||
|
: Results.BadRequest("Failed to create organisation");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Create a new organization profile")
|
||||||
|
.WithName(typeof(CreateOrganisationEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Organisation>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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 DashboardEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/dashboard", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
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 Results.Ok(new
|
||||||
|
{
|
||||||
|
PortfolioValue = totalPortfolioValue,
|
||||||
|
ActiveAwards = activeAwardsCount,
|
||||||
|
Disbursed = disbursedAmount,
|
||||||
|
OnTrackPercentage = onTrackPercentage,
|
||||||
|
AtRiskCount = atRiskCount,
|
||||||
|
BeneficiariesReached = beneficiariesReached
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get executive portfolio dashboard metrics and KPIs")
|
||||||
|
.WithName(typeof(DashboardEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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 GetInstrumentsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/instruments", async (IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var instruments = await context.Instruments.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(instruments);
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get all funding instruments")
|
||||||
|
.WithName(typeof(GetInstrumentsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<List<Domain.Entities.Instrument>>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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 GetOrganisationByIdEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/organisations/{id:long}", async (long id,
|
||||||
|
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var organisation = await context.Organisations.AsNoTracking().FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||||
|
|
||||||
|
return organisation != null ? Results.Ok(organisation) : Results.NotFound();
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get organization profile by ID")
|
||||||
|
.WithName(typeof(GetOrganisationByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Organisation>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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 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) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
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 });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a paginated directory of organizations")
|
||||||
|
.WithName(typeof(GetOrganisationsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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 GetPortfoliosEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/portfolios", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var portfolios = await context.Portfolios.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(portfolios);
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get all portfolios")
|
||||||
|
.WithName(typeof(GetPortfoliosEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<List<Domain.Entities.Portfolio>>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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 GetProgrammesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/governance/programmes", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var programmes = await context.Programmes.AsNoTracking().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(programmes);
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get all programmes")
|
||||||
|
.WithName(typeof(GetProgrammesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<List<Domain.Entities.Programme>>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Governance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
if (request.Amount <= 0)
|
||||||
|
return Results.BadRequest("Approved amount must be greater than zero.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Organisations.AnyAsync(o => o.Id == request.OrganisationId, cancellationToken))
|
||||||
|
return Results.BadRequest($"Organisation with ID {request.OrganisationId} does not exist.");
|
||||||
|
|
||||||
|
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
|
||||||
|
return Results.BadRequest($"Programme with ID {request.ProgrammeId} does not exist.");
|
||||||
|
|
||||||
|
var award = new Domain.Entities.Award
|
||||||
|
{
|
||||||
|
OrganisationId = request.OrganisationId,
|
||||||
|
ProgrammeId = request.ProgrammeId,
|
||||||
|
Amount = request.Amount,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Awards.Add(award);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/grants/awards/{award.Id}", award)
|
||||||
|
: Results.BadRequest("Failred to create award");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Register an approved funding award (M01)")
|
||||||
|
.WithName(typeof(CreateAwardEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Award>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
if (request.Amount <= 0)
|
||||||
|
return Results.BadRequest("Budget amount must be greater than zero.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
|
||||||
|
return Results.BadRequest($"Project with ID {request.ProjectId} does not exist.");
|
||||||
|
|
||||||
|
var budget = new Domain.Entities.Budget
|
||||||
|
{
|
||||||
|
ProjectId = request.ProjectId,
|
||||||
|
Name = request.Name,
|
||||||
|
Amount = request.Amount,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Budgets.Add(budget);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/grants/budgets/{budget.Id}", budget)
|
||||||
|
: Results.BadRequest("Failed to create budget");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Create a new budget line (M05)")
|
||||||
|
.WithName(typeof(CreateBudgetEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Budget>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var award = await context.Awards.FirstOrDefaultAsync(a => a.Id == request.AwardId, cancellationToken);
|
||||||
|
|
||||||
|
if (award == null)
|
||||||
|
return Results.BadRequest($"Award with ID {request.AwardId} does not exist.");
|
||||||
|
|
||||||
|
var contract = new Domain.Entities.Contract
|
||||||
|
{
|
||||||
|
AwardId = request.AwardId,
|
||||||
|
TotalValue = request.TotalValue,
|
||||||
|
EffectiveAt = request.EffectiveAt,
|
||||||
|
ExpiresAt = request.ExpiresAt,
|
||||||
|
Status = ContractStatus.Draft,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Contracts.Add(contract);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/grants/contracts/{contract.Id}", contract)
|
||||||
|
: Results.BadRequest("Failed to create the contract");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Create a contract record from an approved award (M02)")
|
||||||
|
.WithName(typeof(CreateContractEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Contract>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(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");
|
||||||
|
})
|
||||||
|
.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.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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 GetAwardByIdEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/grants/awards/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var award = await context.Awards.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||||
|
|
||||||
|
return award != null ? Results.Ok(award) : Results.NotFound();
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get award by ID")
|
||||||
|
.WithName(typeof(GetAwardByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Award>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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 GetAwardsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/grants/awards", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
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 });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of awards")
|
||||||
|
.WithName(typeof(GetAwardsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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 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,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var budget = await context.Budgets.AsNoTracking().FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
|
||||||
|
|
||||||
|
return budget != null ? Results.Ok(budget) : Results.NotFound();
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get budget by ID")
|
||||||
|
.WithName(typeof(GetBudgetByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Budget>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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 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) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
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 });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get all budgets")
|
||||||
|
.WithName(typeof(GetBudgetsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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 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,
|
||||||
|
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();
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get contract by ID")
|
||||||
|
.WithName(typeof(GetContractByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Contract>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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 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) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
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 });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get all contracts")
|
||||||
|
.WithName(typeof(GetContractsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Grants);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Projects.AnyAsync(p => p.Id == projectId, cancellationToken))
|
||||||
|
return Results.BadRequest($"Project with ID {projectId} does not exist.");
|
||||||
|
|
||||||
|
if (request.MilestoneId.HasValue)
|
||||||
|
if (!await context.Milestones.AnyAsync(m => m.Id == request.MilestoneId.Value, cancellationToken))
|
||||||
|
return Results.BadRequest($"Milestone with ID {request.MilestoneId.Value} does not exist.");
|
||||||
|
|
||||||
|
var activity = new Domain.Entities.Activity
|
||||||
|
{
|
||||||
|
ProjectId = projectId,
|
||||||
|
MilestoneId = request.MilestoneId,
|
||||||
|
Category = request.Category,
|
||||||
|
Amount = request.Amount,
|
||||||
|
Title = request.Title,
|
||||||
|
Description = request.Description,
|
||||||
|
PerformedAt = request.PerformedAt,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Activities.Add(activity);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/projects/{projectId}/activities/{activity.Id}", activity)
|
||||||
|
: Results.BadRequest("Failed to create activity");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Add a new activity under project (M04)")
|
||||||
|
.WithName(typeof(CreateActivityEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Activity>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
|
||||||
|
return Results.BadRequest($"Programme with ID {request.ProgrammeId} does not exist.");
|
||||||
|
|
||||||
|
var project = new Domain.Entities.Project
|
||||||
|
{
|
||||||
|
ProgrammeId = request.ProgrammeId,
|
||||||
|
Name = request.Name,
|
||||||
|
Description = request.Description,
|
||||||
|
Sector = request.Sector,
|
||||||
|
ProjectType = request.ProjectType,
|
||||||
|
Status = ApprovalStatus.Pending,
|
||||||
|
CreatedBy = userId,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Projects.Add(project);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Created($"api/projects/{project.Id}", project)
|
||||||
|
: Results.BadRequest("Failed to create prject");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Create a new project (M04)")
|
||||||
|
.WithName(typeof(CreateProjectEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Project>(StatusCodes.Status201Created)
|
||||||
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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 GetActivitiesEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/projects/{projectId:long}/activities", async (long projectId,
|
||||||
|
IDbContextFactory<ApplicationDbContext> contextFactory, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var activities = await context.Activities.AsNoTracking()
|
||||||
|
.Where(a => a.ProjectId == projectId)
|
||||||
|
.OrderByDescending(a => a.CreatedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Results.Ok(activities);
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of activities for a project")
|
||||||
|
.WithName(typeof(GetActivitiesEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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 GetProjectByIdEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/projects/{id:long}", async (long id, IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var project = await context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||||
|
|
||||||
|
return project != null
|
||||||
|
? Results.Ok(project)
|
||||||
|
: Results.NotFound();
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get project by ID")
|
||||||
|
.WithName(typeof(GetProjectByIdEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Project>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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 GetProjectsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/projects", async (IDbContextFactory<ApplicationDbContext> contextFactory,
|
||||||
|
int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) =>
|
||||||
|
{
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
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 });
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Get a list of projects")
|
||||||
|
.WithName(typeof(GetProjectsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
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) =>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var project = await context.Projects.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||||
|
|
||||||
|
if (project == null)
|
||||||
|
return Results.NotFound($"Project with ID {id} does not exist.");
|
||||||
|
|
||||||
|
project.Status = request.Status;
|
||||||
|
project.UpdatedBy = userId;
|
||||||
|
project.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
context.Projects.Update(project);
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Results.Ok(project)
|
||||||
|
: Results.BadRequest("Failed to update project");
|
||||||
|
})
|
||||||
|
.RequireAuthorization()
|
||||||
|
.WithDescription("Update project status (M04)")
|
||||||
|
.WithName(typeof(UpdateProjectEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Domain.Entities.Project>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
using PostFundManagement.Domain.Api;
|
using PostFundManagement.Domain.Api;
|
||||||
using PostFundManagement.Domain.Extensions;
|
using PostFundManagement.Domain.Extensions;
|
||||||
@@ -10,9 +11,40 @@ public class GetUsersEndpoint : IEndpoint
|
|||||||
{
|
{
|
||||||
public void Map(IEndpointRouteBuilder builder)
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
builder.MapGet("api/users", (IDbContextFactory<ApplicationDbContext> contextFactory) =>
|
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) =>
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100;
|
||||||
|
|
||||||
|
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 });
|
||||||
})
|
})
|
||||||
.RequireAuthorization()
|
.RequireAuthorization()
|
||||||
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
|
.WithDescription("Paginated, searchable directory of system users. Supports filtering by ActivityStatus, searching by Email, and sorting")
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using PostFundManagement.Domain;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Api;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
using PostFundManagement.Infrastructure.Database;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Users;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public class SyncUserEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapPost("api/users/sync", async (ClaimsPrincipal principal,
|
||||||
|
IDbContextFactory<ApplicationDbContext> contextFactory, 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;
|
||||||
|
|
||||||
|
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");
|
||||||
|
})
|
||||||
|
.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.Status401Unauthorized)
|
||||||
|
.WithTags(EndpointTags.Users);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
<Using Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
<Using Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
||||||
<Using Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
<Using Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
<Using Include="Microsoft.IdentityModel.Tokens" />
|
<Using Include="Microsoft.IdentityModel.Tokens" />
|
||||||
|
<Using Include="System.Security.Claims" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Health Checks -->
|
<!-- Health Checks -->
|
||||||
|
|||||||
Reference in New Issue
Block a user