39 Commits
Author SHA1 Message Date
hermes 0d626fd84a Fixed error handling 2026-09-27 14:35:52 +02:00
hermes 9ba48de55b ui tweaks 2026-09-27 14:27:01 +02:00
hermes 6107de3e53 fixed new change request logic 2026-09-27 14:16:08 +02:00
hermes df167caf55 Fixed mismatched endpoint calling 2026-09-27 14:07:44 +02:00
hermes 8afceda1cd Fixed change management rendering error 2026-09-27 13:43:14 +02:00
hermes c1c5641d1e Updated api usage, corrected table designs 2026-09-27 13:31:27 +02:00
hermes 2efd2859c7 Refactored docker compose 2026-09-27 13:07:55 +02:00
hermes dd6996bdb1 Refactored endpoints 2026-09-27 13:06:50 +02:00
hermes 1df0b97daf Issue creation, reolution is optional 2026-09-27 12:55:29 +02:00
hermes e6fdcf3d0d Fixed UI inconsistencies 2026-09-27 12:04:22 +02:00
hermes 36e5d3d7c8 Added, beneficiaries, site visits and change management 2026-09-27 11:40:28 +02:00
hermes fb27cc8c92 Removed settings page mock data and replaced with api calls 2026-09-27 10:58:08 +02:00
hermes 6ab01830f3 Added audit log endpoint 2026-09-27 10:11:56 +02:00
hermes 6e6419ab51 Revised all UIs 2026-09-26 16:36:45 +02:00
hermes ed6e57ffa9 Added budget update
Added portfolio and programme value calculator endpoints
2026-09-26 15:14:29 +02:00
hermes 6be74e9a24 Added postcss 2026-09-26 13:54:39 +02:00
hermes 2949d6218b Implemented data export 2026-09-26 13:45:42 +02:00
hermes 15a8d8f449 Fixed portfolio retrieval 2026-09-26 12:03:47 +02:00
hermes db07ad1a82 Added missing instrument endpoints 2026-09-26 11:20:57 +02:00
hermes 11967e3252 Fixed update organisation 2026-09-26 10:39:22 +02:00
hermes f3a8f78144 Fixed API calls on governance page
Enforced multitenancy
2026-09-26 10:28:43 +02:00
hermes b2688867f3 Surfaced programme rules 2026-09-26 07:55:49 +02:00
hermes 0f903a58b5 Recolved endpoint name ambiguity 2026-09-25 19:06:03 +02:00
hermes 86e2bad4fe Implemented remaining business functionality 2026-09-25 19:00:50 +02:00
hermes 1efbf9d99f Added TenantId to mapper methods on all affected models 2026-09-25 08:20:36 +02:00
hermes 24697776d0 Refactored roganisation routines to use TenantOrganisation 2026-09-25 04:01:31 +02:00
hermes 4b806a3202 UserService refactored to integrate into TenantUser 2026-09-25 03:55:20 +02:00
hermes 26df423180 Refactored cache services 2026-09-25 03:04:37 +02:00
hermes f39eb9d8fe Removed tools feature 2026-09-24 16:00:06 +02:00
hermes 593219db8c Stable run,
Added remaining service implementations (stubs)
2026-09-24 15:57:33 +02:00
hermes 020bdee36f Stable run using Multi tenancy across entire estate
Audit logging and error logging active
2026-09-24 12:00:47 +02:00
hermes 8e0c099867 Stable multitenancy 2026-09-24 10:25:07 +02:00
hermes ef134a5b3c Implemented audit logging and automated logging 2026-09-23 05:33:16 +02:00
hermes 717b891554 Added linkage to primary tenant entities 2026-09-22 07:08:57 +02:00
hermes 91d325ce4e Make retrieval of contract file auditable 2026-09-22 06:28:54 +02:00
hermes 3cc8514136 Added automatic audit feature 2026-09-22 06:09:51 +02:00
hermes 11311421ad Removed ValueTask generally where not needed
Removed relationships from audit log
2026-09-22 05:44:25 +02:00
hermes f4024d563a Implemented centralised audit logging 2026-09-21 16:32:56 +02:00
hermes 78b03b6996 Colocated entity type configurations with entities
Added tenant entities and models
Modified db context
2026-09-21 09:34:49 +02:00
244 changed files with 74256 additions and 9772 deletions
+1
View File
@@ -12,6 +12,7 @@
*.env *.env
*.env.json *.env.json
certificates/ certificates/
package-lock.json
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs
@@ -14,7 +14,7 @@ public sealed class GetApplicantInviteEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapGet("api/communications/invites/applicant/{applicantUserId:Guid}", async (Guid applicantUserId, ICommunicationsService service, builder.MapGet("api/communications/invites/applicants/{applicantUserId:Guid}", async (Guid applicantUserId, ICommunicationsService service,
HttpContext context, CancellationToken cancellationToken = default) => HttpContext context, CancellationToken cancellationToken = default) =>
await service.GetApplicantInviteAsync(applicantUserId, cancellationToken) switch await service.GetApplicantInviteAsync(applicantUserId, cancellationToken) switch
{ {
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using static PostFundManagement.Application.Extensions.General;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Communications;
[ApiVersionTarget(1)]
public sealed class GetNotificationsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/communications/norifications/{organisationId}", async (string organisationId,
ICommunicationsService service, CancellationToken cancellationToken = default) =>
await service.GetNotificationsAsync(organisationId, cancellationToken) switch
{
{ IsSuccess: true, Value: var notifications } => Results.Ok(notifications),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Retrieves the notification system report")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetNotificationsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Notification[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Notification[]>(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Communications)
.CacheOutput();
}
}
@@ -15,7 +15,7 @@ public sealed class SendInviteEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapPost("api/communications/invite", async (InviteRequest request, builder.MapPost("api/communications/invites", async (InviteRequest request,
HttpContext context, ICommunicationsService service, CancellationToken cancellationToken = default) => HttpContext context, ICommunicationsService service, CancellationToken cancellationToken = default) =>
await service.SendInviteAsync(request, context.GetUserId(), cancellationToken) switch await service.SendInviteAsync(request, context.GetUserId(), cancellationToken) switch
{ {
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public sealed class CreateRuleEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/execution/rules", async (CreateRuleRequest request, HttpContext context,
IExecutionService service, CancellationToken cancellationToken = default) =>
await service.CreateRuleAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Add rule against a programme")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateRuleEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
}
}
@@ -3,6 +3,7 @@ using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
@@ -24,7 +25,7 @@ public class GetRisksEndpoint : IEndpoint
.RequireAuthorization(Constants.MainScope) .RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetRisksEndpoint).ToEndpointName()) .WithName(typeof(GetRisksEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces<Risk[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution) .WithTags(EndpointTags.Execution)
@@ -0,0 +1,37 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public sealed class GetRuleEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/execution/rules/{ruleId}", async (string ruleId, IExecutionService service,
CancellationToken cancellationToken = default) =>
await service.GetRuleAsync(ruleId, cancellationToken) switch
{
{ IsSuccess: true, Value: var rules } => Results.Ok(rules),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a specific programme rule")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetRuleEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Rule>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution)
.CacheOutput();
}
}
@@ -0,0 +1,34 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public sealed class GetRulesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/execution/programmes/rules", async (Pagination pagination, IExecutionService service,
CancellationToken cancellationToken = default) =>
await service.GetRulesAsync(pagination, cancellationToken) switch
{
{ IsSuccess: true, Value: var rules } => Results.Ok(rules),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get a list of all programme rules")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetRulesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Rule[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution)
.CacheOutput();
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Execution;
[ApiVersionTarget(1)]
public sealed class UpdateRuleEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/execution/rules", async (UpdateRuleRequest request, HttpContext context,
IExecutionService service, CancellationToken cancellationToken = default) =>
await service.UpdateRuleAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update the details of a specific programme rule (supplied version auto-increment)")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateRuleEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Execution);
}
}
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public sealed class CreateInstrumentEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/governance/instruments", async (CreateInstrumentRequest request, HttpContext context,
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.CreateInstrumentAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true} => Results.Ok(),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Creates a new programme funding instrument")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateInstrumentEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public sealed class CreateTenantEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/governance/tenants", async (CreateTenantRequest request, IGovernanceService service, HttpContext context,
CancellationToken cancellationToken = default) =>
await service.CreateTenantAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Create a new tenant")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateTenantEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance);
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public sealed class GetAuditLogsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/governance/auditlog", async (Pagination pagination, AuditService service,
CancellationToken cancellationToken = default) =>
await service.GetAuditLogsAsync(pagination, cancellationToken) switch
{
{ IsSuccess: true, Value: var logs } => Results.Ok(logs),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Retrieves audit logs based on pagination parameters")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetAuditLogsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<AuditLog[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public class GetOrganisationPortfoliosEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/organisations/{organisationId}/portfolio", async (string organisationId,
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.GetOrganisationPorfoliosAsync(organisationId, cancellationToken) switch
{
{ IsSuccess: true, Value: var portfolios } => Results.Ok(portfolios),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Get all portfolios belonging to a specific organisation")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetOrganisationPortfoliosEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Portfolio[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public sealed class GetPorfolioValueEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/portfolios/{portfolioId}/value", async (string portfolioId, IGovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetPorfolioValueAsync(portfolioId, cancellationToken) switch
{
{ IsSuccess: true, Value: var amount } => Results.Ok(amount),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Calculates and returns a portfolio's total project's budget value")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetPorfolioValueEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<decimal>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -1,7 +1,7 @@
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
@@ -10,16 +10,15 @@ using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance; namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)] [ApiVersionTarget(1)]
public class GetPortfoliosEndpoint : IEndpoint public sealed class GetPortfoliosEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapGet("api/governance/organisations/{organisationId}/portfolio", async (string organisationId, builder.MapPost("api/governance/organisations/portfolios", async (Pagination pagination, IGovernanceService service,
IGovernanceService service, CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
await service.GetPorfoliosAsync(organisationId, cancellationToken) switch await service.GetPorfoliosAsync(pagination, cancellationToken) switch
{ {
{ IsSuccess: true, Value: var portfolios } => Results.Ok(portfolios), { IsSuccess: true, Value: var portfolios } => Results.Ok(portfolios),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Get all portfolios") .WithDescription("Get all portfolios")
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public sealed class GetProgrammeValueEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/programmes/{programmeId}/value", async (string programmeId, IGovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetProgrammeValueAsync(programmeId, cancellationToken) switch
{
{ IsSuccess: true, Value: var amount } => Results.Ok(amount),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Calculates and returns a programme's total project budget value")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetProgrammeValueEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<decimal>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -0,0 +1,37 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public sealed class GetTenantEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/tenants/{tenantId}", async (string tenantId, IGovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetTenantAsync(tenantId, cancellationToken) switch
{
{ IsSuccess: true, Value: var tenant } => Results.Ok(tenant),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets system tenants")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetTenantsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Tenant>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance);
}
}
@@ -0,0 +1,32 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Entities;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public sealed class GetTenantsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/governance/tenants", async (IGovernanceService service,
CancellationToken cancellationToken = default) =>
await service.GetTenantsAsync(cancellationToken) switch
{
{ IsSuccess: true, Value: var tenants } => Results.Ok(tenants),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets system tenants")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetTenantsEndpoint).ToEndpointName("Retrieval"))
.MapToApiVersion(new ApiVersion(1))
.Produces<Tenant[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance);
}
}
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public sealed class UpdateInstrumentEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/governance/instruments", async (UpdateInstrumentRequest request, HttpContext context,
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.UpdateInstrumentAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Updates a programme funding instrument")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateInstrumentEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -15,11 +15,13 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapPut("api/governance/organisations", async (UpdateOrganisationRequest request, builder.MapPut("api/governance/organisations", async (UpdateOrganisationRequest request,
IGovernanceService service, CancellationToken cancellationToken = default) => IGovernanceService service, HttpContext context, CancellationToken cancellationToken = default) =>
await service.UpdateOrganisationAsync(request, cancellationToken) switch await service.UpdateOrganisationAsync(request, context.GetUserId(), cancellationToken) switch
{ {
{ IsSuccess: true } => Results.Ok(), { IsSuccess: true } => Results.Ok(),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()), var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Update an existing organization profile") .WithDescription("Update an existing organization profile")
@@ -27,10 +29,10 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint
.WithName(typeof(UpdateOrganisationEndpoint).ToEndpointName()) .WithName(typeof(UpdateOrganisationEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance); .WithTags(EndpointTags.Governance);
} }
} }
@@ -1,36 +1,36 @@
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security; using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails; using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Tools; namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)] [ApiVersionTarget(1)]
public sealed class EncodeIdEndpoint : IEndpoint public sealed class UpdateTenantEndpoint : IEndpoint
{ {
public void Map(IEndpointRouteBuilder builder) public void Map(IEndpointRouteBuilder builder)
{ {
builder.MapGet("api/tools/hashid/{id64:long}", async (long id64, IToolService service, builder.MapPut("api/governance/tenants", async (UpdateTenantRequest request, HttpContext context,
CancellationToken cancellationToken) => IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.EncodeLong(id64, cancellationToken) switch await service.UpdateTenantAsync(request, context.GetUserId(), cancellationToken) switch
{ {
{ IsSuccess: true, Value: var hash } => Results.Ok(hash), { IsSuccess: true } => Results.Ok(),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()), var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems()) var r => Results.InternalServerError(r.Errors.ToProblems())
}) })
.WithDescription("Hashes an int64 identifier") .WithDescription("Updates a specific tenant")
.RequireAuthorization(Constants.MainScope) .RequireAuthorization(Constants.MainScope)
.WithName(typeof(EncodeIdEndpoint).ToEndpointName()) .WithName(typeof(UpdateTenantEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1)) .MapToApiVersion(new ApiVersion(1))
.Produces<string>(StatusCodes.Status200OK) .Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status400BadRequest) .Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError) .Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Tools) .WithTags(EndpointTags.Governance);
.CacheOutput();
} }
} }
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Grants;
[ApiVersionTarget(1)]
public sealed class UpdateBudgetEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/grants/budgets", async (UpdateBudgetRequest request, HttpContext context,
IGrantsService service, CancellationToken cancellationToken = default) =>
await service.UpdateBudgetAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Updates a specific budget")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateBudgetEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Governance)
.CacheOutput();
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class CreateBeneficiaryEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/beneficiaries", async (CreateBeneficiaryRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.CreateBeneficiaryAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Add a new project beneficiary")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateBeneficiaryEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class CreateChangeRequestEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/changerequests", async (CreateChangeRequest request, IProjectService service,
HttpContext context, CancellationToken cancellationToken = default) =>
await service.CreateChangeRequestAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems()),
})
.WithDescription("Create a new change request against an existing project")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateChangeRequestEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class CreateComplianceItemEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/complianceitems", async (CreateComplianceItemRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.CreateComplianceItemAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Add a new beneficiary compliance item")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateComplianceItemEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class CreateIssueEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/issues", async (CreateIssueRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.CreateIssueAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Add a new project issue")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateIssueEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,36 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class CreateOutcomeEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/beneficiaris/outcomes", async (CreateOutcomeRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.CreateOutcomeAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Add a new beneficiary outcome")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateOutcomeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class CreateSiteVisitEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPost("api/projects/sitevisits", async (CreateSiteVisitRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.CreateSiteVisitAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<CreateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Add a new project site visit")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(CreateSiteVisitEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetBeneficiariesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId}/beneficiaries", async (string projectId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetBeneficiariesAsync(projectId, cancellationToken) switch
{
{ IsSuccess: true, Value: var beneficiaries } => Results.Ok(beneficiaries),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets project beneficiaries")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetBeneficiariesEndpoint).ToEndpointName("Retrieval"))
.MapToApiVersion(new ApiVersion(1))
.Produces<Beneficiary[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetBeneficiaryEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/beneficiaries/{beneficiaryId}", async (string beneficiaryId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetBeneficiaryAsync(beneficiaryId, cancellationToken) switch
{
{ IsSuccess: true, Value: var beneficiary } => Results.Ok(beneficiary),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets project beneficiaries")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetBeneficiariesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Beneficiary>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetChangeRequestEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/changerequests/{changeRequestId}", async (string changeRequestId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetChangeRequestAsync(changeRequestId, cancellationToken) switch
{
{ IsSuccess: true, Value: var changeRequest } => Results.Ok(changeRequest),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a specific project change request")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetChangeRequestEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<ChangeRequest>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetChangeRequestsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId}/changerequests", async (string projectId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetChangeRequestsAsync(projectId, cancellationToken) switch
{
{ IsSuccess: true, Value: var changeRequests } => Results.Ok(changeRequests),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets project change requests")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetChangeRequestsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<ChangeRequest[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetComplianceItemEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/complianceitems/{complianceItemId}", async (string complianceItemId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetComplianceItemAsync(complianceItemId, cancellationToken) switch
{
{ IsSuccess: true, Value: var complianceItem } => Results.Ok(complianceItem),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a specific project compliance item")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetComplianceItemEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<ComplianceItem>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetComplianceItemsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/complianceitems/{projectId}", async (string projectId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetComplianceItemsAsync(projectId, cancellationToken) switch
{
{ IsSuccess: true, Value: var complianceItems } => Results.Ok(complianceItems),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets project compliance items")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetComplianceItemsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<ComplianceItem[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetIssueEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/issues/{issueId}", async (string issueId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetIssueAsync(issueId, cancellationToken) switch
{
{ IsSuccess: true, Value: var issue } => Results.Ok(issue),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a specific project issue")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetIssueEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Issue>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetIssuesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId}/issues", async (string projectId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetIssuesAsync(projectId, cancellationToken) switch
{
{ IsSuccess: true, Value: var issues } => Results.Ok(issues),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets project issues")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetIssuesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Issue[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetOutcomeEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/outcomes/{outcomeId}", async (string outcomeId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetOutcomeAsync(outcomeId, cancellationToken) switch
{
{ IsSuccess: true, Value: var outcome } => Results.Ok(outcome),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets a specific project outcome")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetOutcomeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Outcome>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetOutcomesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/outcomes/{beneficiatyId}", async (string beneficiatyId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetOutcomesAsync(beneficiatyId, cancellationToken) switch
{
{ IsSuccess: true, Value: var outcomes } => Results.Ok(outcomes),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets project beneficiary outcomes")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetOutcomesEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<Outcome[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetSiteVisitEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/sitevisits/{siteVisitId}", async (string siteVisitId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetSiteVisitAsync(siteVisitId, cancellationToken) switch
{
{ IsSuccess: true, Value: var siteVisit } => Results.Ok(siteVisit),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets project site visit")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetSiteVisitEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<SiteVisit>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,38 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class GetSiteVisitsEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/projects/{projectId}/sitevisits", async (string projectId, IProjectService service,
CancellationToken cancellationToken = default) =>
await service.GetSiteVisitsAsync(projectId, cancellationToken) switch
{
{ IsSuccess: true, Value: var siteVisits } => Results.Ok(siteVisits),
var r when r.HasError<IdDecodeError>() => Results.Forbid(),
var r when r.HasError<NotFoundError>() => Results.NotFound(),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Gets project site visits")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(GetSiteVisitsEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<SiteVisit[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects)
.CacheOutput();
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class UpdateBeneficiaryEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects/beneficiaries", async (UpdateBeneficiaryRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.UpdateBeneficiaryAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update project beneficiary")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateBeneficiaryEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class UpdateChangeRequestEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects/changerequests", async (UpdateChangeRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.UpdateChangeRequestAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update a specific project change request")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateChangeRequestEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class UpdateComplianceItemEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects/complianceitems", async (UpdateComplianceItemRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.UpdateComplianceItemAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update a specific compliance item")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateComplianceItemEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class UpdateIssueEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects/issues", async (UpdateIssueRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.UpdateIssueAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update a specific project issue")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateIssueEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class UpdateOutcomeEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects/outcomes", async (UpdateOutcomeRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.UpdateOutcomeAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update project beneficiary outcome")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateOutcomeEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
@@ -0,0 +1,35 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security;
using Problems = Microsoft.AspNetCore.Mvc.ProblemDetails;
namespace PostFundManagement.Api.Endpoints.Projects;
[ApiVersionTarget(1)]
public sealed class UpdateSiteVisitEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/projects/sitevisits", async (UpdateSiteVisitRequest request, HttpContext context,
IProjectService service, CancellationToken cancellationToken = default) =>
await service.UpdateSiteVisitAsync(request, context.GetUserId(), cancellationToken) switch
{
{ IsSuccess: true } => Results.Ok(),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update project site visit")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(UpdateSiteVisitEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Projects);
}
}
+3
View File
@@ -1,8 +1,10 @@
using Microsoft.FeatureManagement; using Microsoft.FeatureManagement;
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Filters; using PostFundManagement.Application.Filters;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Health; using PostFundManagement.Domain.Health;
using PostFundManagement.Domain.Mediator; using PostFundManagement.Domain.Mediator;
using PostFundManagement.Infrastructure.Database.Services;
using static PostFundManagement.Domain.Extensions.Constants; using static PostFundManagement.Domain.Extensions.Constants;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -27,6 +29,7 @@ builder.Services.AddApiVersioning(options =>
}); });
builder.Services.AddScoped<ActiveUserFilter>(); builder.Services.AddScoped<ActiveUserFilter>();
builder.Services.AddTransient<ITenantContextService, TenantContextService>();
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly()); builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
builder.Services.AddApiServices(builder.Configuration); builder.Services.AddApiServices(builder.Configuration);
+5 -2
View File
@@ -20,8 +20,11 @@
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Warning",
"Microsoft.AspNetCore": "Warning" "Quartz": "Error",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database": "Error",
"Microsoft.EntityFrameworkCore.Update": "Error"
} }
}, },
"EmailProcessorCron": "0 */5 * * * ?", "EmailProcessorCron": "0 */5 * * * ?",
@@ -12,8 +12,8 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
public string GenerateCdnUrl(string fileKey) => $"{CdnBaseUrl}/{fileKey}"; public string GenerateCdnUrl(string fileKey) => $"{CdnBaseUrl}/{fileKey}";
public virtual async ValueTask<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default) => public virtual async Task<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () => await Result.Try((Func<Task<Result<string>>>)(async () =>
{ {
if (string.IsNullOrWhiteSpace(BucketName)) if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail(ValidationError.Create("Bucket name is not configured.")); return Result.Fail(ValidationError.Create("Bucket name is not configured."));
@@ -51,8 +51,8 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
: $"{cleanSubPath}/{fileHash.ToLower(CultureInfo.InvariantCulture)}_{fileName}"; : $"{cleanSubPath}/{fileHash.ToLower(CultureInfo.InvariantCulture)}_{fileName}";
} }
public virtual async ValueTask<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default) => public virtual async Task<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () => await Result.Try((Func<Task<Result<string>>>)(async () =>
{ {
if (string.IsNullOrWhiteSpace(BucketName)) if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail<string>(ValidationError.Create("Bucket name is not configured.")); return Result.Fail<string>(ValidationError.Create("Bucket name is not configured."));
@@ -92,8 +92,8 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
: Result.Ok(fileKey); : Result.Ok(fileKey);
})); }));
public virtual async ValueTask<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) => public virtual async Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
if (string.IsNullOrWhiteSpace(BucketName)) if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail(NotFoundError.Create("Bucket name is not configured.")); return Result.Fail(NotFoundError.Create("Bucket name is not configured."));
@@ -1,16 +1,36 @@
using PostFundManagement.Application.Services.Communications; using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface ICommunicationsService public interface ICommunicationsService
{ {
ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default); [Log]
Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default); [Log]
Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default);
[Log]
Task<Result<Notification[]>> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Notification>> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default);
} }
@@ -1,32 +1,50 @@
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IEvidenceService public interface IEvidenceService
{ {
ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default);
ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
Task<Result<Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken); [Log]
Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken);
ValueTask<Result<Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default); [Log]
Task<Result<Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default);
ValueTask<Result<Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default);
} }
@@ -1,26 +1,55 @@
using PostFundManagement.Application.Services.Execution; using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IExecutionService public interface IExecutionService
{ {
ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateRuleAsync(CreateRuleRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> UpdateRuleAsync(UpdateRuleRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
Task<Result<Rule>> GetRuleAsync(string ruleIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
Task<Result<Rule[]>> GetRulesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default); [Log]
Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
} }
@@ -1,32 +1,98 @@
using PostFundManagement.Application.Services.Governance; using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IGovernanceService public interface IGovernanceService
{ {
ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<decimal>> GetProgrammeValueAsync(string programmeIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<decimal>> GetPorfolioValueAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> UpdateInstrumentAsync(UpdateInstrumentRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateInstrumentAsync(CreateInstrumentRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default); [Log]
Task<Result<Portfolio[]>> GetPorfoliosAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
Task<Result<Portfolio[]>> GetOrganisationPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default); [Log]
Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default); Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateTenantAsync(CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Tenant[]>> GetTenantsAsync(CancellationToken cancellationToken = default);
[Log]
Task<Result<Tenant>> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateTenantAsync(UpdateTenantRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateTenantUserAsync(UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateTenantOrganisationAsync(string organisationIdHash, string tenantIdHash, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateTenantOrganisationAsync(string tenantOrganisationIdHash, ActivityStatus status, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<TenantOrganisation[]>> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default);
[Log]
Task<Result<TenantOrganisation>> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default);
} }
@@ -1,34 +1,59 @@
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IGrantsService public interface IGrantsService
{ {
ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> UpdateBudgetAsync(UpdateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default); [Log]
[Audit]
ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default); Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default);
} }
@@ -1,6 +1,10 @@
namespace PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Domain.Attributes;
namespace PostFundManagement.Application.Abstractions.Services;
public interface IIdentityService public interface IIdentityService
{ {
ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default);
} }
@@ -1,46 +1,157 @@
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IProjectService public interface IProjectService
{ {
ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default); [Log]
[Audit]
ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default); Task<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default); [Log]
Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default); [Log]
Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<ChangeRequest[]>> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<ChangeRequest>> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<ComplianceItem[]>> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<ComplianceItem>> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Beneficiary[]>> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Beneficiary>> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Issue[]>> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Issue>> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<Outcome[]>> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Outcome>> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
Task<Result<SiteVisit[]>> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<SiteVisit>> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default);
} }
@@ -2,5 +2,5 @@
public interface IToolService public interface IToolService
{ {
ValueTask<Result<string>> EncodeLong(long id, CancellationToken cancellationToken); Task<Result<string>> EncodeLong(long id, CancellationToken cancellationToken);
} }
@@ -1,16 +1,23 @@
using PostFundManagement.Application.Services.Users; using PostFundManagement.Application.Services.Users;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services; namespace PostFundManagement.Application.Abstractions.Services;
public interface IUserService public interface IUserService
{ {
ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default);
ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default); [Log]
Task<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default); [Log]
[Audit]
Task<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default);
ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken); [Log]
Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken);
} }
@@ -8,7 +8,7 @@ public static class Quartz
{ {
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration) public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{ {
var connectionString = configuration.GetConnectionString(DatabaseConfigName); var connectionString = configuration.GetConnectionString(SchedulerDatabaseConfigName);
services.Configure<QuartzOptions>(options => services.Configure<QuartzOptions>(options =>
{ {
@@ -44,7 +44,7 @@ public static class Quartz
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration) public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{ {
var connectionString = configuration.GetConnectionString(DatabaseConfigName); var connectionString = configuration.GetConnectionString(SchedulerDatabaseConfigName);
services.Configure<QuartzOptions>(options => services.Configure<QuartzOptions>(options =>
{ {
@@ -1,5 +1,6 @@
using PostFundManagement.Application.Abstractions; using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Interceptors;
using PostFundManagement.Application.Services.Communications; using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Application.Services.Evidence; using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Application.Services.Execution; using PostFundManagement.Application.Services.Execution;
@@ -7,7 +8,6 @@ using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Application.Services.Grants; using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Application.Services.Projects; using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Application.Services.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Application.Services.Tools;
using PostFundManagement.Application.Services.Users; using PostFundManagement.Application.Services.Users;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
@@ -15,34 +15,71 @@ namespace PostFundManagement.Application.Extensions;
public static class Services public static class Services
{ {
public static IServiceCollection AddLoggedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
where TCacheDecorator : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped<TCacheDecorator>();
services.AddScoped(sp =>
{
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
var loggingInterceptor = sp.GetRequiredService<LoggingInterceptor>();
var actual = sp.GetRequiredService<TImplementation>();
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
loggingInterceptor.ToInterceptor());
});
return services;
}
public static IServiceCollection AddLoggedAuditedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
where TCacheDecorator : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped<TCacheDecorator>();
services.AddScoped(sp =>
{
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
var auditInterceptor = sp.GetRequiredService<AuditInterceptor>();
var loggingInterceptor = sp.GetRequiredService<LoggingInterceptor>();
var actual = sp.GetRequiredService<TImplementation>();
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
loggingInterceptor.ToInterceptor(),
auditInterceptor.ToInterceptor());
});
return services;
}
public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddScoped<ICommunicationsService, CommunicationsService>(); services.AddScoped<AuditInterceptor>();
services.Decorate<ICommunicationsService, CommunicationsCacheService>(); services.AddScoped<LoggingInterceptor>();
services.AddSingleton<ProxyGenerator>();
services.AddScoped<IEvidenceService, EvidenceService>(); services.AddLoggedAuditedCachedServiceScoped<ICommunicationsService, CommunicationsService, CommunicationsCacheService>();
services.Decorate<IEvidenceService, EvidenceCacheService>(); services.AddLoggedAuditedCachedServiceScoped<IEvidenceService, EvidenceService, EvidenceCacheService>();
services.AddLoggedAuditedCachedServiceScoped<IUserService, UserService, UserCacheService>();
services.AddLoggedAuditedCachedServiceScoped<IExecutionService, ExecutionService, ExecutionCacheService>();
services.AddLoggedAuditedCachedServiceScoped<IGovernanceService, GovernanceService, GovernanceCacheService>();
services.AddLoggedAuditedCachedServiceScoped<IGrantsService, GrantsService, GrantsCacheService>();
services.AddLoggedAuditedCachedServiceScoped<IProjectService, ProjectService, ProjectCacheService>();
services.AddScoped<IUserService, UserService>(); services.AddLoggedCachedServiceScoped<IIdentityService, IdentityService, IdentityCacheService>();
services.Decorate<IUserService, UserCacheService>();
services.AddScoped<IIdentityService, IdentityService>();
services.Decorate<IIdentityService, IdentityCacheService>();
services.AddScoped<IExecutionService, ExecutionService>();
services.Decorate<IExecutionService, ExecutionCacheService>();
services.AddScoped<IGovernanceService, GovernanceService>();
services.Decorate<IGovernanceService, GovernanceCacheService>();
services.AddScoped<IGrantsService, GrantsService>();
services.Decorate<IGrantsService, GrantsCacheService>();
services.AddScoped<IProjectService, ProjectService>();
services.Decorate<IProjectService, ProjectCacheService>();
services.AddScoped<IToolService, ToolService>();
services.Decorate<IToolService, ToolCacheService>();
services.AddStackExchangeRedisCache(options => services.AddStackExchangeRedisCache(options =>
{ {
@@ -25,8 +25,12 @@ public sealed class ActiveUserFilter(IIdentityService identityService) : IEndpoi
var path = context.HttpContext.Request.Path.Value; var path = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrWhiteSpace(path) && !path!.EndsWith("integration/sync", StringComparison.InvariantCultureIgnoreCase)) if (!string.IsNullOrWhiteSpace(path) && !path!.EndsWith("integration/sync", StringComparison.InvariantCultureIgnoreCase))
if ((await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted)).IsFailed) {
return (object?)Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden); var userAuthResult = await identityService.UserIsAuthorisedAsync(userId, context.HttpContext.RequestAborted);
if (userAuthResult.IsFailed)
return Results.Json(new { message = "Account approval is pending or inactive." }, statusCode: StatusCodes.Status403Forbidden);
}
context.HttpContext.Items[UserIdItemKey] = userId; context.HttpContext.Items[UserIdItemKey] = userId;
context.HttpContext.Items[EmailtemKey] = email; context.HttpContext.Items[EmailtemKey] = email;
@@ -0,0 +1,211 @@
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain.Attributes;
namespace PostFundManagement.Application.Interceptors;
public sealed class AuditInterceptor(AuditService auditService, IHttpContextAccessor httpContextAccessor) : IAsyncInterceptor
{
public void InterceptSynchronous(IInvocation invocation) => invocation.Proceed();
public void InterceptAsynchronous(IInvocation invocation) =>
invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
public void InterceptAsynchronous<TResult>(IInvocation invocation) =>
invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
private static async Task InternalInterceptAsynchronous(IInvocation invocation)
{
invocation.Proceed();
var task = (Task)invocation.ReturnValue;
await task.ConfigureAwait(false);
}
private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
{
invocation.Proceed();
var task = (Task<TResult>)invocation.ReturnValue;
var resultObj = await task.ConfigureAwait(false);
var method = invocation.MethodInvocationTarget ?? invocation.Method;
var auditAttr = method.GetCustomAttribute<AuditAttribute>()
?? invocation.Method.GetCustomAttribute<AuditAttribute>();
if (auditAttr == null) return resultObj;
if (resultObj is IResultBase result && result.IsSuccess)
{
try
{
var action = ResolveActionName(method.Name);
var entityName = ResolveEntityName(method, resultObj, invocation.Arguments);
var entityId = ResolveEntityId(resultObj, invocation.Arguments);
var changes = BuildPayloadSummary(invocation.Method.GetParameters(), invocation.Arguments);
var performedBy = ResolveUserId();
await auditService.LogActionAsync(
entityId: entityId,
entityName: entityName,
action: action,
changes: changes,
performedBy: performedBy).ConfigureAwait(false);
}
catch { /* eat all exceptions, don't interrupt program flow */ }
}
return resultObj;
}
private static string ResolveActionName(string methodName) =>
methodName.EndsWith("Async", StringComparison.Ordinal)
? methodName[..^5]
: methodName;
private static string ResolveEntityName(MethodInfo method, object? resultObj, object[] args)
{
var returnType = method.ReturnType;
Type? innerType = null;
if (returnType.IsGenericType)
{
var genericDef = returnType.GetGenericTypeDefinition();
if (genericDef == typeof(Task<>) || genericDef == typeof(Task<>))
innerType = returnType.GetGenericArguments()[0];
}
if (innerType != null && innerType.IsGenericType && typeof(IResultBase).IsAssignableFrom(innerType))
{
var valueType = innerType.GetGenericArguments().FirstOrDefault();
if (valueType != null && valueType != typeof(bool) && valueType != typeof(int) && valueType != typeof(string))
return valueType.Name;
}
foreach (var arg in args)
{
if (arg == null || arg is CancellationToken or IFormFile) continue;
var argTypeName = arg.GetType().Name;
if (argTypeName.EndsWith("Request", StringComparison.Ordinal))
{
var trimmed = argTypeName[..^7];
return trimmed.Replace("Create", "").Replace("Update", "");
}
}
return ResolveActionName(method.Name);
}
private static string ResolveEntityId(object? resultObj, object[] args)
{
if (resultObj != null)
{
var valueProp = resultObj.GetType().GetProperty("Value");
if (valueProp != null)
{
var value = valueProp.GetValue(resultObj);
if (value != null)
{
var idProp = value.GetType().GetProperty("Id") ?? value.GetType().GetProperty("id");
if (idProp != null)
{
var idVal = idProp.GetValue(value);
if (idVal != null) return idVal.ToString()!;
}
}
}
}
foreach (var arg in args)
{
if (arg is long or int or Guid or string)
return arg.ToString()!;
if (arg != null && arg is not CancellationToken and not IFormFile)
{
var idProp = arg.GetType().GetProperty("Id") ?? arg.GetType().GetProperty("id");
if (idProp != null)
{
var idVal = idProp.GetValue(arg);
if (idVal != null) return idVal.ToString()!;
}
}
}
return "N/A";
}
private static string[] BuildPayloadSummary(ParameterInfo[] parameters, object[] args)
{
var entries = new List<string>();
for (var i = 0; i < parameters.Length; i++)
{
var param = parameters[i];
var arg = args[i];
if (param.ParameterType == typeof(CancellationToken))
continue;
if (arg == null)
{
entries.Add($"{param.Name}: null");
continue;
}
if (arg is IFormFile file)
{
entries.Add($"{param.Name}: [File] Name='{file.FileName}', Size={file.Length} bytes, Type='{file.ContentType}'");
continue;
}
if (arg is IEnumerable<IFormFile> files)
{
var fileSummaries = files.Select(f => $"'{f.FileName}' ({f.Length} bytes)");
entries.Add($"{param.Name}: [Files] count={fileSummaries.Count()} [{string.Join(", ", fileSummaries)}]");
continue;
}
if (arg is Stream stream)
{
entries.Add($"{param.Name}: [Stream] Length={(stream.CanSeek ? stream.Length : -1)} bytes");
continue;
}
try
{
var json = JsonSerializer.Serialize(arg);
entries.Add($"{param.Name}: {json}");
}
catch
{
entries.Add($"{param.Name}: [Unserializable]");
}
}
return [.. entries];
}
private Guid ResolveUserId()
{
var user = httpContextAccessor.HttpContext?.User;
var sub = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? user?.FindFirst("sub")?.Value;
return Guid.TryParse(sub, out var guid) ? guid : Guid.Empty;
}
}
@@ -0,0 +1,136 @@
using PostFundManagement.Domain.Attributes;
namespace PostFundManagement.Application.Interceptors;
public sealed class LoggingInterceptor(ILogger<LoggingInterceptor> logger) : IAsyncInterceptor
{
public void InterceptSynchronous(IInvocation invocation)
{
invocation.Proceed();
CheckAndLogIfFailed(invocation, invocation.ReturnValue);
}
public void InterceptAsynchronous(IInvocation invocation) =>
invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
public void InterceptAsynchronous<TResult>(IInvocation invocation) =>
invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
private static async Task InternalInterceptAsynchronous(IInvocation invocation)
{
invocation.Proceed();
var task = (Task)invocation.ReturnValue;
await task.ConfigureAwait(false);
}
private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
{
invocation.Proceed();
var task = (Task<TResult>)invocation.ReturnValue;
var resultObj = await task.ConfigureAwait(false);
var method = invocation.MethodInvocationTarget ?? invocation.Method;
var auditAttr = method.GetCustomAttribute<LogAttribute>()
?? invocation.Method.GetCustomAttribute<LogAttribute>();
if (auditAttr == null) return resultObj;
CheckAndLogIfFailed(invocation, resultObj);
return resultObj;
}
private void CheckAndLogIfFailed(IInvocation invocation, object? resultObj)
{
if (resultObj is IResultBase result && result.IsFailed)
{
try
{
var method = invocation.MethodInvocationTarget ?? invocation.Method;
var parameters = BuildPayloadSummary(method.GetParameters(), invocation.Arguments);
var errorMessages = new List<string>();
if (result.Reasons != null)
{
foreach (var reason in result.Reasons)
if (reason is IError error)
errorMessages.Add(error.Message);
}
var errorsString = string.Join("; ", errorMessages);
logger.LogError("Method {MethodName} failed. Errors: [{ErrorType}] {Errors}. Parameters: {@Parameters}",
method.Name, result.Reasons?.FirstOrDefault()?.GetType().Name.Replace("Error", string.Empty), errorsString, parameters);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to write error log for method {MethodName}.", invocation.Method.Name);
}
}
}
private static string[] BuildPayloadSummary(ParameterInfo[] parameters, object[] args)
{
var entries = new List<string>();
for (var i = 0; i < parameters.Length; i++)
{
var param = parameters[i];
var arg = args[i];
if (param.ParameterType == typeof(CancellationToken))
continue;
if (arg == null)
{
entries.Add($"{param.Name}: null");
continue;
}
if (arg is IFormFile file)
{
entries.Add($"{param.Name}: [File] Name='{file.FileName}', Size={file.Length} bytes, Type='{file.ContentType}'");
continue;
}
if (arg is IEnumerable<IFormFile> files)
{
var fileSummaries = new List<string>();
foreach (var f in files)
fileSummaries.Add($"'{f.FileName}' ({f.Length} bytes)");
entries.Add($"{param.Name}: [Files] count={fileSummaries.Count} [{string.Join(", ", fileSummaries)}]");
continue;
}
if (arg is Stream stream)
{
entries.Add($"{param.Name}: [Stream] Length={(stream.CanSeek ? stream.Length : -1)} bytes");
continue;
}
try
{
var json = JsonSerializer.Serialize(arg);
entries.Add($"{param.Name}: {json}");
}
catch
{
entries.Add($"{param.Name}: [Unserializable]");
}
}
return [.. entries];
}
}
@@ -24,6 +24,7 @@
<!-- API Documentation --> <!-- API Documentation -->
<ItemGroup> <ItemGroup>
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.12" /> <PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.12" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.4" /> <PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
@@ -36,6 +37,7 @@
<!-- Shared Usings --> <!-- Shared Usings -->
<ItemGroup> <ItemGroup>
<Using Include="Humanizer" /> <Using Include="Humanizer" />
<Using Include="Castle.DynamicProxy" />
<Using Include="Microsoft.FeatureManagement" /> <Using Include="Microsoft.FeatureManagement" />
<Using Include="System.ComponentModel.DataAnnotations" /> <Using Include="System.ComponentModel.DataAnnotations" />
<Using Include="Microsoft.Extensions.Caching.Distributed" /> <Using Include="Microsoft.Extensions.Caching.Distributed" />
@@ -11,8 +11,11 @@ namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsCacheService(CommunicationsService service, IDistributedCache cache, public sealed class CommunicationsCacheService(CommunicationsService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, ICommunicationsService IFeatureManager featureManager) : ServiceCacheBase, ICommunicationsService
{ {
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => public async Task<Result> CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () => await service.CreateNotificationAsync(request, userId, cancellationToken);
public async Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Invite>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetApplicantInviteAsync(applicantUserId, cancellationToken); return await service.GetApplicantInviteAsync(applicantUserId, cancellationToken);
@@ -33,8 +36,8 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
return Result.Ok(cached!.FromJson<Invite>()); return Result.Ok(cached!.FromJson<Invite>());
})); }));
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => public async Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () => await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken); return await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken);
@@ -57,8 +60,8 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
return Result.Ok(cached.FromJson<Invite[]>()); return Result.Ok(cached.FromJson<Invite[]>());
})); }));
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () => await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetInvitesAsync(pagination, cancellationToken); return await service.GetInvitesAsync(pagination, cancellationToken);
@@ -81,6 +84,57 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
return Result.Ok(cached.FromJson<Invite[]>()); return Result.Ok(cached.FromJson<Invite[]>());
})); }));
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result<Notification>> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Notification>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetNotificationAsync(notificationIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getnotification-{notificationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetNotificationAsync(notificationIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return Result.Ok(result.Value);
}
return Result.Ok(cached.FromJson<Notification>());
}));
public async Task<Result<Notification[]>> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Notification[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetNotificationsAsync(organisationIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getnotifications-{organisationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetNotificationsAsync(organisationIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return Result.Ok(result.Value);
}
return Result.Ok(cached.FromJson<Notification[]>());
}));
public async Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.SendInviteAsync(request, userId, cancellationToken); await service.SendInviteAsync(request, userId, cancellationToken);
public async Task<Result> UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default) =>
await service.UpdateNotificationAsync(request, cancellationToken);
} }
@@ -1,5 +1,7 @@
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
@@ -7,10 +9,11 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Communications; namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory) : ICommunicationsService public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext> contextFactory,
HashService hashService, ITenantContextService tenantContext) : ICommunicationsService
{ {
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () => await Result.Try((Func<Task<Result<Invite>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -22,28 +25,24 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
ContractId = request.ContractId, ContractId = request.ContractId,
Recipient = request.Recipient, Recipient = request.Recipient,
CreatedBy = userId, CreatedBy = userId,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow,
TenantId = tenantContext.TenantId
}; };
context.Invites.Add(invite); context.Invites.Add(invite);
context.Notifications.Add(new Domain.Entities.Notification var notificationRequest = new CreateNotificationRequest(request.InvitedOrganisationId, request.Recipient,
{ request.Platform, "Invitation to Onboard", string.Empty, request.Message);
OrganisationId = request.OrganisationId,
Recipient = request.Recipient,
Platform = request.Platform,
Status = NotificationStatus.Pending,
Subject = "Invitation to Onboard",
Message = request.Message,
CreatedAt = DateTime.UtcNow
});
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New) var notificationResult = await CreateNotificationAsync(notificationRequest, userId, cancellationToken);
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0 && notificationResult.IsFailed,
CreateError.Create().CausedBy(notificationResult.Errors))
.ToResult(invite.Map()); .ToResult(invite.Map());
})); }));
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () => await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -56,8 +55,8 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
return Result.Ok(invites.Select(i => i.Map()).ToArray()); return Result.Ok(invites.Select(i => i.Map()).ToArray());
})); }));
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken) => public async Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () => await Result.Try((Func<Task<Result<Invite>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -67,16 +66,91 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
.ToResult(invite!.Map()); .ToResult(invite!.Map());
})); }));
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) => public async Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () => await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var invites = await context.Invites.AsNoTracking() var invites = await context.Invites.AsNoTracking()
.Where(i => i.Recipient == applicantUserId) .Where(i => i.Recipient == applicantUserId)
.OrderByDescending(o => o.CreatedAt) .OrderByDescending(o => o.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(invites.Select(i => i.Map()).ToArray()); return Result.Ok(invites.Select(i => i.Map()).ToArray());
})); }));
public async Task<Result> CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
context.Notifications.Add(new Domain.Entities.Notification
{
CreatedAt = DateTime.UtcNow,
Recipient = request.Recipient,
OrganisationId = request.OrganisationId,
Destination = request.Destination,
Subject = request.Subject,
Message = request.Message,
Platform = request.Platform,
Status = NotificationStatus.Queued,
TenantId = tenantContext.TenantId
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create entry"));
}));
public async Task<Result> UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Notifications
.Where(n => n.Id == request.NotificationId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Destination, request.Destination)
.SetProperty(u => u.SentAt, request.SentAt)
.SetProperty(u => u.Error, request.Error)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow)
.SetProperty(u => u.Status, request.Status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create($"Failed to update notification: {request.NotificationId}"));
}));
public async Task<Result<Notification[]>> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Notification[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(organisationIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var notifications = await context.Notifications.AsNoTracking()
.Where(n => n.OrganisationId == decodeResult.Value)
.OrderByDescending(o => o.Id)
.ToListAsync(cancellationToken);
return Result.Ok(notifications?.Select(i => i.Map()).ToArray() ?? []);
}));
public async Task<Result<Notification>> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Notification>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(notificationIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var notification = await context.Notifications.AsNoTracking()
.FirstOrDefaultAsync(n => n.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(notification is null, NotFoundError.New)
.ToResult(notification!.Map());
}));
} }
@@ -3,6 +3,18 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Communications; namespace PostFundManagement.Application.Services.Communications;
public sealed record UpdateNotificationRequest([Required] long NotificationId, string? Destination,
[Required] NotificationStatus Status,
[Required] DateTime SentAt,
[MaxLength(TextLength)] string? Error);
public sealed record CreateNotificationRequest([Required] long OrganisationId,
[Required] Guid Recipient,
[Required] NotificationPlatform Platform,
[Required][MaxLength(LabelLength)] string? Subject,
[Required][MaxLength(LabelLength)] string? Destination,
[Required][MaxLength(TextLength)] string Message);
public sealed record InviteRequest([Required] long OrganisationId, public sealed record InviteRequest([Required] long OrganisationId,
[Required] long InvitedOrganisationId, [Required] long InvitedOrganisationId,
[Required] long AwardId, [Required] long AwardId,
@@ -11,17 +11,17 @@ namespace PostFundManagement.Application.Services.Evidence;
public sealed class EvidenceCacheService(EvidenceService service, IDistributedCache cache, public sealed class EvidenceCacheService(EvidenceService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IEvidenceService IFeatureManager featureManager) : ServiceCacheBase, IEvidenceService
{ {
public async ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) => public async Task<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
await service.CreateEvidenceAsync(userId, form, file, cancellationToken); await service.CreateEvidenceAsync(userId, form, file, cancellationToken);
public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateIndicatorAsync(request, userId, cancellationToken); await service.CreateIndicatorAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateMilestoneAsync(request, userId, cancellationToken); await service.CreateMilestoneAsync(request, userId, cancellationToken);
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Evidence>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceAsync(evidenceId, cancellationToken); return await service.GetEvidenceAsync(evidenceId, cancellationToken);
@@ -44,8 +44,8 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached.FromJson<Domain.Models.Evidence>()); return Result.Ok(cached.FromJson<Domain.Models.Evidence>());
})); }));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceFilesAsync(pagination, cancellationToken); return await service.GetEvidenceFilesAsync(pagination, cancellationToken);
@@ -68,8 +68,8 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>()); return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>());
})); }));
public async ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) => public async Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () => await Result.Try((Func<Task<Result<string>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceUrlAsync(evidenceIdHash, cancellationToken); return await service.GetEvidenceUrlAsync(evidenceIdHash, cancellationToken);
@@ -92,8 +92,8 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached); return Result.Ok(cached);
})); }));
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken = default) => public async Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () => await Result.Try((Func<Task<Result<Indicator>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIndicatorAsync(indicatorId, cancellationToken); return await service.GetIndicatorAsync(indicatorId, cancellationToken);
@@ -116,8 +116,8 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached.FromJson<Indicator>()); return Result.Ok(cached.FromJson<Indicator>());
})); }));
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () => await Result.Try((Func<Task<Result<Indicator[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIndicatorsAsync(pagination, cancellationToken); return await service.GetIndicatorsAsync(pagination, cancellationToken);
@@ -140,8 +140,8 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached.FromJson<Indicator[]>()); return Result.Ok(cached.FromJson<Indicator[]>());
})); }));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetMilestoneEvidencesAsync(milestoneIdHash, cancellationToken); return await service.GetMilestoneEvidencesAsync(milestoneIdHash, cancellationToken);
@@ -164,8 +164,8 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>()); return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>());
})); }));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectEvidencesAsync(projectIdHash, cancellationToken); return await service.GetProjectEvidencesAsync(projectIdHash, cancellationToken);
@@ -188,9 +188,9 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>()); return Result.Ok(cached.FromJson<Domain.Models.Evidence[]>());
})); }));
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) =>
await service.RecordIndicatorActualAmountAsync(indicatorIdHash, amount, userId, cancellationToken); await service.RecordIndicatorActualAmountAsync(indicatorIdHash, amount, userId, cancellationToken);
public async ValueTask<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateEvidenceAsync(request, userId, cancellationToken); await service.UpdateEvidenceAsync(request, userId, cancellationToken);
} }
@@ -12,8 +12,8 @@ namespace PostFundManagement.Application.Services.Evidence;
public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService, public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
[FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service) : IEvidenceService [FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service) : IEvidenceService
{ {
public async ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) => public async Task<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
if (!long.TryParse(form["projectId"], out var projectId) || !long.TryParse(form["milestoneId"], out var milestoneId) || if (!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) || !long.TryParse(form["indicatorId"], out var indicatorId) || !long.TryParse(form["activityId"], out var activityId) ||
@@ -49,8 +49,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash); var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash);
@@ -68,8 +68,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.OkIf(changedTotal > 0, UpdateError.New); return Result.OkIf(changedTotal > 0, UpdateError.New);
})); }));
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () => await Result.Try((Func<Task<Result<Indicator[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -82,8 +82,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.Ok(indicators.Select(i => i.Map()).ToArray()); return Result.Ok(indicators.Select(i => i.Map()).ToArray());
})); }));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -96,8 +96,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.Ok(evidence.Select(i => i.Map()).ToArray()); return Result.Ok(evidence.Select(i => i.Map()).ToArray());
})); }));
public async ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -122,8 +122,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
CreateError.Create("No record could be created.")); CreateError.Create("No record could be created."));
})); }));
public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -145,8 +145,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) => public async Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () => await Result.Try((Func<Task<Result<Indicator>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(indicatorId); var decodeResult = hashService.DecodeLongIdHash(indicatorId);
@@ -162,8 +162,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.ToResult(indicator!.Map()); .ToResult(indicator!.Map());
})); }));
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Evidence>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(evidenceId); var decodeResult = hashService.DecodeLongIdHash(evidenceId);
@@ -179,8 +179,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.ToResult(evidence!.Map()); .ToResult(evidence!.Map());
})); }));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash);
@@ -199,8 +199,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.Ok(evidences.Select(e => e.Map()).ToArray()); return Result.Ok(evidences.Select(e => e.Map()).ToArray());
})); }));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
@@ -219,8 +219,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.Ok(evidences.Select(e => e.Map()).ToArray()); return Result.Ok(evidences.Select(e => e.Map()).ToArray());
})); }));
public async ValueTask<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -238,8 +238,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.OkIf(changedTotal > 0, UpdateError.New); return Result.OkIf(changedTotal > 0, UpdateError.New);
})); }));
public async ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) => public async Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () => await Result.Try((Func<Task<Result<string>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(evidenceIdHash); var decodeResult = hashService.DecodeLongIdHash(evidenceIdHash);
@@ -11,17 +11,20 @@ namespace PostFundManagement.Application.Services.Execution;
public sealed class ExecutionCacheService(ExecutionService service, IDistributedCache cache, public sealed class ExecutionCacheService(ExecutionService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IExecutionService IFeatureManager featureManager) : ServiceCacheBase, IExecutionService
{ {
public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) =>
await service.ApproveDisbursementAsync(disbursementIdHash, userId, cancellationToken); await service.ApproveDisbursementAsync(disbursementIdHash, userId, cancellationToken);
public async ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateDisbursementAsync(request, userId, cancellationToken); await service.CreateDisbursementAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateRiskAsync(request, userId, cancellationToken); await service.CreateRiskAsync(request, userId, cancellationToken);
public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default) => public async Task<Result> CreateRuleAsync(CreateRuleRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement>>>)(async () => await service.CreateRuleAsync(request, userId, cancellationToken);
public async Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Disbursement>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDisbursementAsync(disbursementIdHash, cancellationToken); return await service.GetDisbursementAsync(disbursementIdHash, cancellationToken);
@@ -44,8 +47,8 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
return Result.Ok(cached.FromJson<Disbursement>()); return Result.Ok(cached.FromJson<Disbursement>());
})); }));
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement[]>>>)(async () => await Result.Try((Func<Task<Result<Disbursement[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDisbursementsAsync(pagination, cancellationToken); return await service.GetDisbursementsAsync(pagination, cancellationToken);
@@ -68,8 +71,8 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
return Result.Ok(cached.FromJson<Disbursement[]>()); return Result.Ok(cached.FromJson<Disbursement[]>());
})); }));
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk>>>)(async () => await Result.Try((Func<Task<Result<Risk>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRiskAsync(riskIdHash, cancellationToken); return await service.GetRiskAsync(riskIdHash, cancellationToken);
@@ -92,8 +95,8 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
return Result.Ok(cached.FromJson<Risk>()); return Result.Ok(cached.FromJson<Risk>());
})); }));
public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () => await Result.Try((Func<Task<Result<Risk[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRisksAsync(pagination, cancellationToken); return await service.GetRisksAsync(pagination, cancellationToken);
@@ -116,9 +119,60 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
return Result.Ok(cached.FromJson<Risk[]>()); return Result.Ok(cached.FromJson<Risk[]>());
})); }));
public async ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result<Rule>> GetRuleAsync(string ruleIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Rule>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRuleAsync(ruleIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getrisk-{ruleIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetRuleAsync(ruleIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Rule>());
}));
public async Task<Result<Rule[]>> GetRulesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Rule[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRulesAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getrules-{pagination.Page}-{pagination.PageSize}-{pagination.Index}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetRulesAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Rule[]>());
}));
public async Task<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateDisbursementAsync(request, userId, cancellationToken); await service.UpdateDisbursementAsync(request, userId, cancellationToken);
public async ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default) => public async Task<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default) =>
await service.UpdateRiskAsync(request, cancellationToken); await service.UpdateRiskAsync(request, cancellationToken);
public async Task<Result> UpdateRuleAsync(UpdateRuleRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateRuleAsync(request, userId, cancellationToken);
} }
@@ -1,6 +1,7 @@
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
@@ -8,10 +9,11 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Execution; namespace PostFundManagement.Application.Services.Execution;
public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IExecutionService public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
ITenantContextService tenantContext) : IExecutionService
{ {
public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () => await Result.Try((Func<Task<Result<Risk[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -21,11 +23,11 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(risks.Select(i => i.Map()).ToArray()); return Result.Ok(risks?.Select(i => i.Map()).ToArray() ?? []);
})); }));
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement[]>>>)(async () => await Result.Try((Func<Task<Result<Disbursement[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -35,11 +37,11 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToArrayAsync(cancellationToken); .ToArrayAsync(cancellationToken);
return Result.Ok(disbursements.Select(i => i.Map()).ToArray()); return Result.Ok(disbursements?.Select(i => i.Map()).ToArray() ?? []);
})); }));
public async ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -60,8 +62,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -81,8 +83,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash); var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash);
@@ -93,16 +95,16 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Disbursements.Where(d => d.Id == decodeResult.Value) var changedTotal = await context.Disbursements.Where(d => d.Id == decodeResult.Value)
.ExecuteUpdateAsync(f => .ExecuteUpdateAsync(f => f
f.SetProperty(f => f.Status, ApprovalStatus.Approved) .SetProperty(f => f.Status, ApprovalStatus.Approved)
.SetProperty(f => f.UpdatedBy, userId) .SetProperty(f => f.UpdatedBy, userId)
.SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken); .SetProperty(f => f.UpdatedAt, DateTime.UtcNow), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.New); return Result.OkIf(changedTotal > 0, UpdateError.New);
})); }));
public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken) => public async Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Disbursement>>>)(async () => await Result.Try((Func<Task<Result<Disbursement>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash); var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash);
@@ -119,8 +121,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.ToResult(disbursement!.Map()); .ToResult(disbursement!.Map());
})); }));
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk>>>)(async () => await Result.Try((Func<Task<Result<Risk>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(riskIdHash); var decodeResult = hashService.DecodeLongIdHash(riskIdHash);
@@ -137,8 +139,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.ToResult(risk!.Map()); .ToResult(risk!.Map());
})); }));
public async ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default) => public async Task<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -153,8 +155,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.OkIf(changedTotal > 0, UpdateError.New); return Result.OkIf(changedTotal > 0, UpdateError.New);
})); }));
public async ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -168,4 +170,82 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.OkIf(changedTotal > 0, UpdateError.New); return Result.OkIf(changedTotal > 0, UpdateError.New);
})); }));
public async Task<Result> CreateRuleAsync(CreateRuleRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Programmes.AnyAsync(p => p.Id == request.ProgrammeId, cancellationToken))
return Result.Fail(ValidationError.Create("Programme does not exist"));
var requestedName = request.Name.Humanize(LetterCasing.Title).Trim();
if (await context.Rules.AnyAsync(r => r.Name == requestedName && r.ProgrammeId == request.ProgrammeId, cancellationToken))
return Result.Fail(ValidationError.Create("A rule with the same name exists"));
context.Rules.Add(new Domain.Entities.Rule
{
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
Description = request.Description,
Name = requestedName,
ProgrammeId = request.ProgrammeId,
Status = ActivityStatus.Active,
Version = 1,
TenantId = tenantContext.TenantId
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create rule"));
}));
public async Task<Result> UpdateRuleAsync(UpdateRuleRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var requestedName = request.Name.Humanize(LetterCasing.Title).Trim();
var changedTotal = await context.Rules
.Where(f => f.Id == request.RuleId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Status, request.Status)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow)
.SetProperty(u => u.Name, requestedName)
.SetProperty(u => u.Version, request.Version + 1)
.SetProperty(u => u.Description, request.Description), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update rule"));
}));
public async Task<Result<Rule>> GetRuleAsync(string ruleIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Rule>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(ruleIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Rule>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rule = await context.Rules.AsNoTracking().FirstOrDefaultAsync(r => r.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(rule is null, NotFoundError.New)
.ToResult(rule!.Map());
}));
public async Task<Result<Rule[]>> GetRulesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Rule[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rules = await context.Rules.AsNoTracking()
.OrderByDescending(d => d.Id)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToArrayAsync(cancellationToken);
return Result.Ok(rules?.Select(i => i.Map()).ToArray() ?? []);
}));
} }
@@ -3,6 +3,18 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Execution; namespace PostFundManagement.Application.Services.Execution;
public sealed record UpdateRuleRequest([Required] long RuleId,
[Required] Guid UserId,
[Required][MaxLength(TextLength)] string Name,
[Required][MaxLength(TextLength)] string Description,
[Required] ActivityStatus Status,
[Required] int Version);
public sealed record CreateRuleRequest([Required] long ProgrammeId,
[Required] Guid UserId,
[Required][MaxLength(TextLength)] string Name,
[Required][MaxLength(TextLength)] string Description);
public sealed record UpdateDisbursementRequest([Required] long DisbursementId, long? MilestoneId, [Required] decimal Amount); public sealed record UpdateDisbursementRequest([Required] long DisbursementId, long? MilestoneId, [Required] decimal Amount);
public sealed record UpdateRiskRequest([Required] long RiskId, public sealed record UpdateRiskRequest([Required] long RiskId,
@@ -11,14 +11,23 @@ namespace PostFundManagement.Application.Services.Governance;
public sealed class GovernanceCacheService(GovernanceService service, IDistributedCache cache, public sealed class GovernanceCacheService(GovernanceService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IGovernanceService IFeatureManager featureManager) : ServiceCacheBase, IGovernanceService
{ {
public async ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateInstrumentAsync(CreateInstrumentRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateInstrumentAsync(request, userId, cancellationToken);
public async Task<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateOrganisationAsync(request, userId, cancellationToken); await service.CreateOrganisationAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreatePortfolioAsync(request, userId, cancellationToken); await service.CreatePortfolioAsync(request, userId, cancellationToken);
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) => public async Task<Result> CreateTenantAsync(CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Dashboard>>>)(async () => await service.CreateTenantAsync(request, userId, cancellationToken);
public async Task<Result> CreateTenantOrganisationAsync(string organisationIdHash, string tenantIdHash, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateTenantOrganisationAsync(organisationIdHash, tenantIdHash, userId, cancellationToken);
public async Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Dashboard>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDashboardAsync(cancellationToken); return await service.GetDashboardAsync(cancellationToken);
@@ -41,8 +50,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Dashboard>()); return Result.Ok(cached.FromJson<Dashboard>());
})); }));
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () => await Result.Try((Func<Task<Result<Instrument[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetInstrumentsAsync(programmeIdHash, cancellationToken); return await service.GetInstrumentsAsync(programmeIdHash, cancellationToken);
@@ -65,8 +74,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Instrument[]>()); return Result.Ok(cached.FromJson<Instrument[]>());
})); }));
public async ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () => await Result.Try((Func<Task<Result<Organisation>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOrganisationAsync(organisationIdHash, cancellationToken); return await service.GetOrganisationAsync(organisationIdHash, cancellationToken);
@@ -89,8 +98,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Organisation>()); return Result.Ok(cached.FromJson<Organisation>());
})); }));
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation[]>>>)(async () => await Result.Try((Func<Task<Result<Organisation[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOrganisationsAsync(pagination, cancellationToken); return await service.GetOrganisationsAsync(pagination, cancellationToken);
@@ -113,19 +122,19 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Organisation[]>()); return Result.Ok(cached.FromJson<Organisation[]>());
})); }));
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Portfolio[]>> GetOrganisationPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () => await Result.Try((Func<Task<Result<Portfolio[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPorfoliosAsync(organisationIdHash, cancellationToken); return await service.GetOrganisationPorfoliosAsync(organisationIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getportfolios-{organisationIdHash}").Value; var cacheKey = HashService.ToMd5Hash($"getorganisationportfolios-{organisationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken); var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached)) if (string.IsNullOrWhiteSpace(cached))
{ {
var result = await service.GetPorfoliosAsync(organisationIdHash, cancellationToken); var result = await service.GetOrganisationPorfoliosAsync(organisationIdHash, cancellationToken);
if (result.IsFailed) return result; if (result.IsFailed) return result;
@@ -137,8 +146,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Portfolio[]>()); return Result.Ok(cached.FromJson<Portfolio[]>());
})); }));
public async ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio>>>)(async () => await Result.Try((Func<Task<Result<Portfolio>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPortfolioAsync(portfolioIdHash, cancellationToken); return await service.GetPortfolioAsync(portfolioIdHash, cancellationToken);
@@ -161,8 +170,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Portfolio>()); return Result.Ok(cached.FromJson<Portfolio>());
})); }));
public async ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () => await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPortfolioProgrammesAsync(portfolioIdHash, cancellationToken); return await service.GetPortfolioProgrammesAsync(portfolioIdHash, cancellationToken);
@@ -185,8 +194,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Programme[]>()); return Result.Ok(cached.FromJson<Programme[]>());
})); }));
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () => await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammesAsync(portfolioIdHash, cancellationToken); return await service.GetProgrammesAsync(portfolioIdHash, cancellationToken);
@@ -209,9 +218,193 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Programme[]>()); return Result.Ok(cached.FromJson<Programme[]>());
})); }));
public async ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) => public async Task<Result<Tenant>> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) =>
await service.UpdateOrganisationAsync(request, cancellationToken); await Result.Try((Func<Task<Result<Tenant>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetTenantAsync(tenantIdHash, cancellationToken);
public async ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => var cacheKey = HashService.ToMd5Hash($"gettenant-{tenantIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetTenantAsync(tenantIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Tenant>());
}));
public async Task<Result<TenantOrganisation>> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<TenantOrganisation>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetTenantOrganisationAsync(tenantOrganisationIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"gettenantorganisation-{tenantOrganisationIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetTenantOrganisationAsync(tenantOrganisationIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<TenantOrganisation>());
}));
public async Task<Result<TenantOrganisation[]>> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<TenantOrganisation[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetTenantOrganisationsAsync(cancellationToken);
var cacheKey = HashService.ToMd5Hash($"gettenantorganisations").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetTenantOrganisationsAsync(cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<TenantOrganisation[]>());
}));
public async Task<Result<Tenant[]>> GetTenantsAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Tenant[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetTenantsAsync(cancellationToken);
var cacheKey = HashService.ToMd5Hash($"gettenants").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetTenantsAsync(cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Tenant[]>());
}));
public async Task<Result> UpdateInstrumentAsync(UpdateInstrumentRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateInstrumentAsync(request, userId, cancellationToken);
public async Task<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateOrganisationAsync(request, userId, cancellationToken);
public async Task<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdatePortfolioAsync(request, userId, cancellationToken); await service.UpdatePortfolioAsync(request, userId, cancellationToken);
public async Task<Result> UpdateTenantAsync(UpdateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateTenantAsync(request, userId, cancellationToken);
public async Task<Result> UpdateTenantOrganisationAsync(string tenantOrganisationIdHash, ActivityStatus status, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateTenantOrganisationAsync(tenantOrganisationIdHash, status, userId, cancellationToken);
public async Task<Result> UpdateTenantUserAsync(UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateTenantUserAsync(request, userId, cancellationToken);
public async Task<Result<Portfolio[]>> GetPorfoliosAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Portfolio[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPorfoliosAsync(pagination, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getportfolios-{pagination.Page}-{pagination.PageSize}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetPorfoliosAsync(pagination, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Portfolio[]>());
}));
public async Task<Result<decimal>> GetProgrammeValueAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<decimal>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammeValueAsync(programmeIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getprogrammevalue-{programmeIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetProgrammeValueAsync(programmeIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToString(), cacheOptions, cancellationToken);
return result.Value;
}
decimal.TryParse(cached, CultureInfo.InvariantCulture, out var value);
return Result.Ok(value);
}));
public async Task<Result<decimal>> GetPorfolioValueAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<decimal>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPorfolioValueAsync(portfolioIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getportfoliovalue-{portfolioIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetPorfolioValueAsync(portfolioIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToString(), cacheOptions, cancellationToken);
return result.Value;
}
decimal.TryParse(cached, CultureInfo.InvariantCulture, out var value);
return Result.Ok(value);
}));
} }
@@ -1,18 +1,20 @@
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database; using PostFundManagement.Infrastructure.Database;
using PostFundManagement.Application.Extensions;
namespace PostFundManagement.Application.Services.Governance; namespace PostFundManagement.Application.Services.Governance;
public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IGovernanceService public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
ITenantContextService tenantContext) : IGovernanceService
{ {
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () => await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash); var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash);
@@ -30,8 +32,8 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.Ok(programmes.Select(p => p.Map()).ToArray()); return Result.Ok(programmes.Select(p => p.Map()).ToArray());
})); }));
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Portfolio[]>> GetOrganisationPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () => await Result.Try((Func<Task<Result<Portfolio[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(organisationIdHash); var decodeResult = hashService.DecodeLongIdHash(organisationIdHash);
@@ -42,29 +44,31 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var portfolios = await context.Portfolios.AsNoTracking() var portfolios = await context.Portfolios.AsNoTracking()
.Where(f => f.Id == decodeResult.Value) .Include(i => i.Organisation)
.Where(f => f.OrganisationId == decodeResult.Value)
.OrderByDescending(o => o.CreatedAt) .OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(portfolios.Select(p => p.Map()).ToArray()); return Result.Ok(portfolios.Select(p => p.Map()).ToArray());
})); }));
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation[]>>>)(async () => await Result.Try((Func<Task<Result<Organisation[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var organisations = await context.Organisations.AsNoTracking() var organisations = await context.TenantOrganisations.AsNoTracking()
.OrderBy(o => o.Name) .Include(i => i.Organisation)
.OrderBy(o => o.Organisation!.Name)
.Skip((pagination.Page - 1) * pagination.PageSize) .Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize) .Take(pagination.PageSize)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(organisations.Select(o => o.Map()).ToArray()); return Result.Ok(organisations.Select(o => o.Organisation!.Map()).ToArray());
})); }));
public async ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () => await Result.Try((Func<Task<Result<Organisation>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(organisationIdHash); var decodeResult = hashService.DecodeLongIdHash(organisationIdHash);
@@ -74,15 +78,16 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var organisation = await context.Organisations.AsNoTracking() var tenantOrganisation = await context.TenantOrganisations.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken); .Include(i => i.OrganisationId)
.FirstOrDefaultAsync(o => o.OrganisationId == decodeResult.Value, cancellationToken);
return Result.FailIf(organisation is null, NotFoundError.New) return Result.FailIf(tenantOrganisation is null, NotFoundError.New)
.ToResult(organisation!.Map()); .ToResult(tenantOrganisation!.Organisation!.Map());
})); }));
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () => await Result.Try((Func<Task<Result<Instrument[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(programmeIdHash); var decodeResult = hashService.DecodeLongIdHash(programmeIdHash);
@@ -100,8 +105,8 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.Ok(instruments.Select(i => i.Map()).ToArray()); return Result.Ok(instruments.Select(i => i.Map()).ToArray());
})); }));
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) => public async Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Dashboard>>>)(async () => await Result.Try((Func<Task<Result<Dashboard>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -126,8 +131,8 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
disbursedAmount, onTrackPercentage, atRiskCount, beneficiariesReached)); disbursedAmount, onTrackPercentage, atRiskCount, beneficiariesReached));
})); }));
public async ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -136,7 +141,7 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
if (await context.Organisations.AnyAsync(o => o.RegistrationNo == registrationNo, cancellationToken)) if (await context.Organisations.AnyAsync(o => o.RegistrationNo == registrationNo, cancellationToken))
return Result.Fail(CreateError.Create("Organisation with the same registration number already exists")); return Result.Fail(CreateError.Create("Organisation with the same registration number already exists"));
context.Organisations.Add(new Domain.Entities.Organisation var organisation = context.Organisations.Add(new Domain.Entities.Organisation
{ {
Name = request.Name.Humanize(LetterCasing.Title).Trim(), Name = request.Name.Humanize(LetterCasing.Title).Trim(),
RegistrationNo = registrationNo, RegistrationNo = registrationNo,
@@ -147,11 +152,16 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); });
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); await context.SaveChangesAsync(cancellationToken);
var mapResult = await CreateTenantOrganisationAsync(hashService.HashEncodeLongId(organisation.Entity.Id).Value,
hashService.HashEncodeLongId(tenantContext.TenantId).Value, userId, cancellationToken);
return Result.FailIf(mapResult.IsFailed, CreateError.Create("Failed to create organisation"));
})); }));
public async ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) => public async Task<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () => await Result.Try((Func<Task<Result<Organisation>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -161,13 +171,21 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()) .SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.RegistrationNo, request.RegistrationNo.Clean()) .SetProperty(u => u.RegistrationNo, request.RegistrationNo.Clean())
.SetProperty(u => u.Email, request.Email.ToLower(CultureInfo.InvariantCulture).Clean()) .SetProperty(u => u.Email, request.Email.ToLower(CultureInfo.InvariantCulture).Clean())
.SetProperty(u => u.Type, request.Type), cancellationToken); .SetProperty(u => u.Type, request.Type)
.SetProperty(u => u.Status, request.Status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.New); var tenantOrganisation = await context.TenantOrganisations.AsNoTracking().FirstOrDefaultAsync(f => f.OrganisationId == request.OrganisationId, cancellationToken);
if (tenantOrganisation is null)
return Result.Fail(ValidationError.Create("Failed to find linked tenant organisation"));
var updateResult = await UpdateTenantOrganisationAsync(hashService.HashEncodeLongId(tenantOrganisation.Id).Value, request.Status, userId, cancellationToken);
return Result.OkIf(changedTotal > 0 && updateResult.IsSuccess, UpdateError.Create("Failed to update organisation"));
})); }));
public async ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -189,14 +207,15 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
OrganisationId = request.OrganisationId, OrganisationId = request.OrganisationId,
OwnedBy = request.OwnerId, OwnedBy = request.OwnerId,
Name = name, Name = name,
Description = request.Description.Humanize(LetterCasing.Sentence).Trim() Description = request.Description.Humanize(LetterCasing.Sentence).Trim(),
TenantId = tenantContext.TenantId
}); });
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -213,8 +232,8 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.OkIf(changedTotal > 0, UpdateError.Create("Nothing was saved, no change detected.")); return Result.OkIf(changedTotal > 0, UpdateError.Create("Nothing was saved, no change detected."));
})); }));
public async ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () => await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash); var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash);
@@ -232,8 +251,8 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.Ok(programmes.Select(p => p.Map()).ToArray()); return Result.Ok(programmes.Select(p => p.Map()).ToArray());
})); }));
public async ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio>>>)(async () => await Result.Try((Func<Task<Result<Portfolio>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash); var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash);
@@ -249,4 +268,263 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.FailIf(portfolio is null, NotFoundError.New) return Result.FailIf(portfolio is null, NotFoundError.New)
.ToResult(portfolio!.Map()); .ToResult(portfolio!.Map());
})); }));
public async Task<Result> CreateTenantAsync(CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var requestedName = request.Name.Humanize(LetterCasing.Title).Trim();
if (await context.Tenants.AnyAsync(t => t.Name == requestedName, cancellationToken))
return Result.Fail(ValidationError.Create("A tenant with an equivalent name exists."));
context.Tenants.Add(new Domain.Entities.Tenant
{
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
Name = requestedName,
Status = ActivityStatus.Active
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0,
CreateError.Create("Failed to create new tenant"));
}));
public async Task<Result<Tenant[]>> GetTenantsAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Tenant[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var tenants = await context.Tenants.AsNoTracking()
.OrderByDescending(o => o.Id)
.ToListAsync(cancellationToken);
return Result.Ok(tenants?.Select(t => t.Map()).ToArray() ?? []);
}));
public async Task<Result<Tenant>> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Tenant>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(tenantIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var tenant = await context.Tenants.AsNoTracking().FirstOrDefaultAsync(t => t.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(tenant is null, NotFoundError.Create("Failed to retrieve tenants"))
.ToResult(tenant!.Map());
}));
public async Task<Result> UpdateTenantAsync(UpdateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var requestedName = request.Name.Humanize(LetterCasing.Title).Trim();
if (await context.Tenants.AnyAsync(t => t.Name == requestedName, cancellationToken))
return Result.Fail(ValidationError.Create("A tenant with an equivalent name exists."));
var changedTotal = await context.Tenants
.Where(f => f.Id == request.TenantId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow)
.SetProperty(u => u.UpdatedBy, userId)
.SetProperty(u => u.Status, request.Status)
.SetProperty(u => u.Name, requestedName), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records updated"));
}));
public async Task<Result> UpdateTenantUserAsync(UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.TenantUsers
.Where(f => f.UserId == request.UserId)
.ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, request.Status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records updated"));
}));
public async Task<Result> CreateTenantOrganisationAsync(string organisationIdHash, string tenantIdHash, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
var organisationDecodeResult = hashService.DecodeLongIdHash(organisationIdHash);
if (organisationDecodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(organisationDecodeResult.Errors));
var tenantDecodeResult = hashService.DecodeLongIdHash(tenantIdHash);
if (tenantDecodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(tenantDecodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.TenantOrganisations.AnyAsync(to => to.OrganisationId == organisationDecodeResult.Value && to.TenantId == tenantDecodeResult.Value, cancellationToken))
return Result.Fail(ValidationError.Create("Already exists"));
context.TenantOrganisations.Add(new Domain.Entities.TenantOrganisation
{
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
OrganisationId = organisationDecodeResult.Value,
TenantId = tenantDecodeResult.ValueOrDefault
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0,
CreateError.Create("Failed to create tenant organisation map"));
}));
public async Task<Result> UpdateTenantOrganisationAsync(string tenantOrganisationIdHash, ActivityStatus status, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(tenantOrganisationIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.TenantOrganisations
.Where(f => f.Id == decodeResult.Value)
.ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update tenant organisation map"));
}));
public async Task<Result<TenantOrganisation[]>> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<TenantOrganisation[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var tenantOrganisations = await context.TenantOrganisations.AsNoTracking()
.Include(i => i.Organisation)
.Include(i => i.Tenant)
.Where(f => f.TenantId == tenantContext.TenantId)
.ToListAsync();
return Result.Ok(tenantOrganisations?.Select(i => i.Map()).ToArray() ?? []);
}));
public async Task<Result<TenantOrganisation>> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<TenantOrganisation>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(tenantOrganisationIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var tenantOrganisation = await context.TenantOrganisations.AsNoTracking()
.Include(i => i.Organisation)
.Include(i => i.Tenant)
.FirstOrDefaultAsync(f => f.TenantId == tenantContext.TenantId, cancellationToken);
return Result.FailIf(tenantOrganisation is null, NotFoundError.Create("Failed to find tenant organisation map"))
.ToResult(tenantOrganisation!.Map());
}));
public async Task<Result> UpdateInstrumentAsync(UpdateInstrumentRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Instruments
.Where(i => i.Id == request.InstructionId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.Code, request.Code)
.SetProperty(u => u.Description, request.Description)
.SetProperty(u => u.Status, request.Status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update instrument"));
}));
public async Task<Result> CreateInstrumentAsync(CreateInstrumentRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
context.Instruments.Add(new Domain.Entities.Instrument
{
Code = request.Code,
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
Description = request.Description,
Name = request.Name.Humanize(LetterCasing.Title).Trim(),
Status = ActivityStatus.Active,
ProgrammeId = request.ProgrammeId,
TenantId = tenantContext.TenantId
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create instrument"));
}));
public async Task<Result<Portfolio[]>> GetPorfoliosAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Portfolio[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var portfolios = await context.Portfolios.AsNoTracking()
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(portfolios.Select(p => p.Map()).ToArray());
}));
public async Task<Result<decimal>> GetProgrammeValueAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<decimal>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(programmeIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var value = await context.Budgets.AsNoTracking()
.Include(i => i.Project!.Programme)
.Where(v => v.Project!.Programme!.Id == decodeResult.Value &&
v.Project.Programme.Status == ApprovalStatus.Approved &&
v.Status == ApprovalStatus.Approved)
.SumAsync(v => v.Amount, cancellationToken);
return Result.Ok(value);
}));
public async Task<Result<decimal>> GetPorfolioValueAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<decimal>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash);
if (decodeResult.IsFailed)
return Result.Fail(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var value = await context.Budgets
.Include(i => i.Project!.Programme!.Portfolio)
.Where(v => v.Project!.Programme!.Portfolio!.Id == decodeResult.Value &&
v.Project.Programme.Status == ApprovalStatus.Approved &&
v.Status == ApprovalStatus.Approved)
.SumAsync(v => v.Amount, cancellationToken);
return Result.Ok(value);
}));
} }
@@ -3,25 +3,47 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Governance; namespace PostFundManagement.Application.Services.Governance;
public sealed record UpdateInstrumentRequest([Required] long InstructionId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string Code,
[Required][MaxLength(TextLength)] string Description,
[Required] ActivityStatus Status);
public sealed record CreateInstrumentRequest([Required] long ProgrammeId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string Code,
[Required][MaxLength(TextLength)] string Description);
public sealed record CreateTenantRequest([Required][MaxLength(LabelLength)] string Name);
public sealed record UpdateTenantUserRequest([Required] Guid UserId, [Required] ActivityStatus Status);
public sealed record CreateTenantUserRequest([Required] long TenantId, [Required] Guid UserId);
public sealed record UpdateTenantRequest([Required] long TenantId,
[Required][MaxLength(LabelLength)] string Name,
[Required] ActivityStatus Status);
public sealed record UpdatePortfolioRequest([Required] long PortfolioId, public sealed record UpdatePortfolioRequest([Required] long PortfolioId,
[Required][MaxLength(LabelLength)] string Name, [Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description, [Required][MaxLength(TextLength)] string Description,
[Required] Guid OwnerId); [Required] Guid OwnerId);
public sealed record CreatePortfolioRequest([Required][MaxLength(LabelLength)] string Name, public sealed record CreatePortfolioRequest([Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description, [Required][MaxLength(TextLength)] string Description,
[Required] long OrganisationId, [Required] long OrganisationId,
[Required] Guid OwnerId); [Required] Guid OwnerId);
public sealed record UpdateOrganisationRequest([Required] long OrganisationId, public sealed record UpdateOrganisationRequest([Required] long OrganisationId,
[Required][MaxLength(LabelLength)] string Name, [Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string RegistrationNo, [Required][MaxLength(LabelLength)] string RegistrationNo,
[Required][EmailAddress][MaxLength(LabelLength)] string Email, [Required][EmailAddress][MaxLength(LabelLength)] string Email,
[Required] OrganisationType Type); [Required] OrganisationType Type,
[Required] ActivityStatus Status);
public sealed record CreateOrganisationRequest([Required][MaxLength(LabelLength)] string Name, public sealed record CreateOrganisationRequest([Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string RegistrationNo, [Required][MaxLength(LabelLength)] string RegistrationNo,
[Required][EmailAddress][MaxLength(LabelLength)] string Email, [Required][EmailAddress][MaxLength(LabelLength)] string Email,
[Required] OrganisationType Type = OrganisationType.PrivateLimitedCompany); [Required] OrganisationType Type = OrganisationType.PrivateLimitedCompany);
public sealed record Dashboard([Required] decimal PortfolioValue, public sealed record Dashboard([Required] decimal PortfolioValue,
@@ -11,20 +11,20 @@ namespace PostFundManagement.Application.Services.Grants;
public sealed class GrantsCacheService(GrantsService service, IDistributedCache cache, public sealed class GrantsCacheService(GrantsService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IGrantsService IFeatureManager featureManager) : ServiceCacheBase, IGrantsService
{ {
public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateAwardAsync(request, userId, cancellationToken); await service.CreateAwardAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateBudgetAsync(request, userId, cancellationToken); await service.CreateBudgetAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateContractAsync(request, userId, cancellationToken); await service.CreateContractAsync(request, userId, cancellationToken);
public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.ExecuteContractAsync(request, userId, cancellationToken); await service.ExecuteContractAsync(request, userId, cancellationToken);
public async ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award>>>)(async () => await Result.Try((Func<Task<Result<Award>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetAwardAsync(awardIdHash, cancellationToken); return await service.GetAwardAsync(awardIdHash, cancellationToken);
@@ -47,8 +47,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Award>()); return Result.Ok(cached.FromJson<Award>());
})); }));
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award[]>>>)(async () => await Result.Try((Func<Task<Result<Award[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetAwardsAsync(pagination, cancellationToken); return await service.GetAwardsAsync(pagination, cancellationToken);
@@ -71,8 +71,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Award[]>()); return Result.Ok(cached.FromJson<Award[]>());
})); }));
public async ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget>>>)(async () => await Result.Try((Func<Task<Result<Budget>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBudgetAsync(budgetIdHash, cancellationToken); return await service.GetBudgetAsync(budgetIdHash, cancellationToken);
@@ -95,8 +95,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Budget>()); return Result.Ok(cached.FromJson<Budget>());
})); }));
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget[]>>>)(async () => await Result.Try((Func<Task<Result<Budget[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBudgetsAsync(pagination, cancellationToken); return await service.GetBudgetsAsync(pagination, cancellationToken);
@@ -119,8 +119,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Budget[]>()); return Result.Ok(cached.FromJson<Budget[]>());
})); }));
public async ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract>>>)(async () => await Result.Try((Func<Task<Result<Contract>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractAsync(contractIdHash, cancellationToken); return await service.GetContractAsync(contractIdHash, cancellationToken);
@@ -143,8 +143,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Contract>()); return Result.Ok(cached.FromJson<Contract>());
})); }));
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () => await Result.Try((Func<Task<Result<Contract[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractsAsync(pagination, cancellationToken); return await service.GetContractsAsync(pagination, cancellationToken);
@@ -167,8 +167,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Contract[]>()); return Result.Ok(cached.FromJson<Contract[]>());
})); }));
public async ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) => public async Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () => await Result.Try((Func<Task<Result<string>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractUrlAsync(contractIdHash, cancellationToken); return await service.GetContractUrlAsync(contractIdHash, cancellationToken);
@@ -191,9 +191,12 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached!); return Result.Ok(cached!);
})); }));
public async ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) => public async Task<Result> UpdateBudgetAsync(UpdateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateBudgetAsync(request, userId, cancellationToken);
public async Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
await service.UpdateContractAsync(request, cancellationToken); await service.UpdateContractAsync(request, cancellationToken);
public async ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) => public async Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) =>
await service.UploadContractAsync(contractIdHash, userId, file, cancellationToken); await service.UploadContractAsync(contractIdHash, userId, file, cancellationToken);
} }
@@ -10,10 +10,10 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Grants; namespace PostFundManagement.Application.Services.Grants;
public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService, public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
[FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service) : IGrantsService [FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service, ITenantContextService tenantContext) : IGrantsService
{ {
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () => await Result.Try((Func<Task<Result<Contract[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -26,8 +26,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.Ok(contracts.Select(c => c.Map()).ToArray()); return Result.Ok(contracts.Select(c => c.Map()).ToArray());
})); }));
public async ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract>>>)(async () => await Result.Try((Func<Task<Result<Contract>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(contractIdHash); var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
@@ -44,8 +44,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.ToResult(contract!.Map()); .ToResult(contract!.Map());
})); }));
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget[]>>>)(async () => await Result.Try((Func<Task<Result<Budget[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -58,8 +58,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.Ok(budgets.Select(b => b.Map()).ToArray()); return Result.Ok(budgets.Select(b => b.Map()).ToArray());
})); }));
public async ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget>>>)(async () => await Result.Try((Func<Task<Result<Budget>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(budgetIdHash); var decodeResult = hashService.DecodeLongIdHash(budgetIdHash);
@@ -76,8 +76,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.ToResult(budget!.Map()); .ToResult(budget!.Map());
})); }));
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award[]>>>)(async () => await Result.Try((Func<Task<Result<Award[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -90,8 +90,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.Ok(awards.Select(a => a.Map()).ToArray()); return Result.Ok(awards.Select(a => a.Map()).ToArray());
})); }));
public async ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award>>>)(async () => await Result.Try((Func<Task<Result<Award>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(awardIdHash); var decodeResult = hashService.DecodeLongIdHash(awardIdHash);
@@ -107,8 +107,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.ToResult(award!.Map()); .ToResult(award!.Map());
})); }));
public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -123,8 +123,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, UpdateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, UpdateError.New);
})); }));
public async ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -150,8 +150,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
if (request.Amount <= 0) if (request.Amount <= 0)
return Result.Fail(ValidationError.Create("Budget amount must be greater than zero.")); return Result.Fail(ValidationError.Create("Budget amount must be greater than zero."));
@@ -167,7 +167,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
Name = request.Name, Name = request.Name,
Amount = request.Amount, Amount = request.Amount,
CreatedBy = userId, CreatedBy = userId,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow,
Status = ApprovalStatus.Pending
}; };
context.Budgets.Add(budget); context.Budgets.Add(budget);
@@ -175,8 +176,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
if (request.Amount <= 0) if (request.Amount <= 0)
return Result.Fail(ValidationError.Create("Approved amount must be greater than zero.")); return Result.Fail(ValidationError.Create("Approved amount must be greater than zero."));
@@ -196,7 +197,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
Amount = request.Amount, Amount = request.Amount,
Status = ApprovalStatus.Pending, Status = ApprovalStatus.Pending,
CreatedBy = userId, CreatedBy = userId,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow,
TenantId = tenantContext.TenantId
}; };
context.Awards.Add(award); context.Awards.Add(award);
@@ -204,8 +206,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) => public async Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () => await Result.Try((Func<Task<Result<string>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(contractIdHash); var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
@@ -231,8 +233,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.ToResult(secureDownloadUrlResult.Value); .ToResult(secureDownloadUrlResult.Value);
})); }));
public async ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) => public async Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(contractIdHash); var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
@@ -261,8 +263,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
CreateError.Create("Failed to upload contract file")); CreateError.Create("Failed to upload contract file"));
})); }));
public async ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) => public async Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -272,4 +274,19 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update the contract.")); return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update the contract."));
})); }));
public async Task<Result> UpdateBudgetAsync(UpdateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Budgets
.Where(f => f.Id == request.BudgetId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.ApprovedBy, userId)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow)
.SetProperty(u => u.Status, request.Status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update budget"));
}));
} }
@@ -3,6 +3,8 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Grants; namespace PostFundManagement.Application.Services.Grants;
public sealed record UpdateBudgetRequest([Required] long BudgetId, [Required] ApprovalStatus Status);
public sealed record UpdateContractRequest([Required] long ContractId, [Required] ContractStatus Status); public sealed record UpdateContractRequest([Required] long ContractId, [Required] ContractStatus Status);
public sealed record CreateAwardRequest([Required] long OrganisationId, [Required] long ProgrammeId, [Required] decimal Amount); public sealed record CreateAwardRequest([Required] long OrganisationId, [Required] long ProgrammeId, [Required] decimal Amount);
@@ -11,17 +11,35 @@ namespace PostFundManagement.Application.Services.Projects;
public sealed class ProjectCacheService(ProjectService service, IDistributedCache cache, public sealed class ProjectCacheService(ProjectService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IProjectService IFeatureManager featureManager) : ServiceCacheBase, IProjectService
{ {
public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateActivityAsync(request, userId, cancellationToken); await service.CreateActivityAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateBeneficiaryAsync(request, userId, cancellationToken);
public async Task<Result> CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateChangeRequestAsync(request, userId, cancellationToken);
public async Task<Result> CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateComplianceItemAsync(request, userId, cancellationToken);
public async Task<Result> CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateIssueAsync(request, userId, cancellationToken);
public async Task<Result> CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateOutcomeAsync(request, userId, cancellationToken);
public async Task<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateProgrammeAsync(request, userId, cancellationToken); await service.CreateProgrammeAsync(request, userId, cancellationToken);
public async ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.CreateProjectAsync(request, userId, cancellationToken); await service.CreateProjectAsync(request, userId, cancellationToken);
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result> CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity[]>>>)(async () => await service.CreateSiteVisitAsync(request, userId, cancellationToken);
public async Task<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Activity[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetActivitiesAsync(projectIdHash, cancellationToken); return await service.GetActivitiesAsync(projectIdHash, cancellationToken);
@@ -44,8 +62,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Domain.Models.Activity[]>()); return Result.Ok(cached.FromJson<Domain.Models.Activity[]>());
})); }));
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Activity>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetActivityAsync(activityIdHash, cancellationToken); return await service.GetActivityAsync(activityIdHash, cancellationToken);
@@ -68,8 +86,200 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Domain.Models.Activity>()); return Result.Ok(cached.FromJson<Domain.Models.Activity>());
})); }));
public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Beneficiary[]>> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone>>>)(async () => await Result.Try((Func<Task<Result<Beneficiary[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBeneficiariesAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getbeneficiaries-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetBeneficiariesAsync(projectIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Beneficiary[]>());
}));
public async Task<Result<Beneficiary>> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Beneficiary>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBeneficiaryAsync(beneficiaryIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getbeneficiary-{beneficiaryIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetBeneficiaryAsync(beneficiaryIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Beneficiary>());
}));
public async Task<Result<ChangeRequest>> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ChangeRequest>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetChangeRequestAsync(changeRequestIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getchangerequest-{changeRequestIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetChangeRequestAsync(changeRequestIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<ChangeRequest>());
}));
public async Task<Result<ChangeRequest[]>> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ChangeRequest[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetChangeRequestsAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getchangerequests-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetChangeRequestsAsync(projectIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<ChangeRequest[]>());
}));
public async Task<Result<ComplianceItem>> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ComplianceItem>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetComplianceItemAsync(complianceItemIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getcomplianceitem-{complianceItemIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetComplianceItemAsync(complianceItemIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<ComplianceItem>());
}));
public async Task<Result<ComplianceItem[]>> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ComplianceItem[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetComplianceItemsAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getcomplianceitems-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetComplianceItemsAsync(projectIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<ComplianceItem[]>());
}));
public async Task<Result<Issue>> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Issue>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIssueAsync(issueIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getissue-{issueIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetIssueAsync(issueIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Issue>());
}));
public async Task<Result<Issue[]>> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Issue[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIssuesAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getissues-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetIssuesAsync(projectIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Issue[]>());
}));
public async Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Milestone>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetMilestoneAsync(milestoneIdHash, cancellationToken); return await service.GetMilestoneAsync(milestoneIdHash, cancellationToken);
@@ -92,8 +302,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Milestone>()); return Result.Ok(cached.FromJson<Milestone>());
})); }));
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () => await Result.Try((Func<Task<Result<Milestone[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetMilestonesAsync(pagination, cancellationToken); return await service.GetMilestonesAsync(pagination, cancellationToken);
@@ -116,8 +326,56 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Milestone[]>()); return Result.Ok(cached.FromJson<Milestone[]>());
})); }));
public async ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Outcome>> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme>>>)(async () => await Result.Try((Func<Task<Result<Outcome>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOutcomeAsync(outcomeIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getoutcome-{outcomeIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetOutcomeAsync(outcomeIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Outcome>());
}));
public async Task<Result<Outcome[]>> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Outcome[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOutcomesAsync(beneficiaryIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getoutcomes-{beneficiaryIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetOutcomesAsync(beneficiaryIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<Outcome[]>());
}));
public async Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Programme>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammeAsync(programmeIdHash, cancellationToken); return await service.GetProgrammeAsync(programmeIdHash, cancellationToken);
@@ -140,8 +398,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached!.FromJson<Programme>()); return Result.Ok(cached!.FromJson<Programme>());
})); }));
public async ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () => await Result.Try((Func<Task<Result<Project[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammeProjectsAsync(programmeIdHash, cancellationToken); return await service.GetProgrammeProjectsAsync(programmeIdHash, cancellationToken);
@@ -164,8 +422,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Project[]>()); return Result.Ok(cached.FromJson<Project[]>());
})); }));
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () => await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammesAsync(pagination, cancellationToken); return await service.GetProgrammesAsync(pagination, cancellationToken);
@@ -188,8 +446,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Programme[]>()); return Result.Ok(cached.FromJson<Programme[]>());
})); }));
public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project>>>)(async () => await Result.Try((Func<Task<Result<Project>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectAsync(projectIdHash, cancellationToken); return await service.GetProjectAsync(projectIdHash, cancellationToken);
@@ -212,8 +470,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Project>()); return Result.Ok(cached.FromJson<Project>());
})); }));
public async ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () => await Result.Try((Func<Task<Result<Indicator[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectIndicatorsAsync(projectIdHash, cancellationToken); return await service.GetProjectIndicatorsAsync(projectIdHash, cancellationToken);
@@ -236,8 +494,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Indicator[]>()); return Result.Ok(cached.FromJson<Indicator[]>());
})); }));
public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () => await Result.Try((Func<Task<Result<Milestone[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectMilestonesAsync(projectIdHash, cancellationToken); return await service.GetProjectMilestonesAsync(projectIdHash, cancellationToken);
@@ -260,8 +518,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Milestone[]>()); return Result.Ok(cached.FromJson<Milestone[]>());
})); }));
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () => await Result.Try((Func<Task<Result<Risk[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectRisksAsync(projectIdHash, cancellationToken); return await service.GetProjectRisksAsync(projectIdHash, cancellationToken);
@@ -284,8 +542,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Risk[]>()); return Result.Ok(cached.FromJson<Risk[]>());
})); }));
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () => await Result.Try((Func<Task<Result<Project[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectsAsync(pagination, cancellationToken); return await service.GetProjectsAsync(pagination, cancellationToken);
@@ -308,15 +566,81 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Project[]>()); return Result.Ok(cached.FromJson<Project[]>());
})); }));
public async ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result<SiteVisit>> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<SiteVisit>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetSiteVisitAsync(siteVisitIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getsitevisit-{siteVisitIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetSiteVisitAsync(siteVisitIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<SiteVisit>());
}));
public async Task<Result<SiteVisit[]>> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<SiteVisit[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetSiteVisitsAsync(projectIdHash, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"getsitevisits-{projectIdHash}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if (string.IsNullOrWhiteSpace(cached))
{
var result = await service.GetSiteVisitsAsync(projectIdHash, cancellationToken);
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached.FromJson<SiteVisit[]>());
}));
public async Task<Result> UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateBeneficiaryAsync(request, userId, cancellationToken);
public async Task<Result> UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateChangeRequestAsync(request, userId, cancellationToken);
public async Task<Result> UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateComplianceItemAsync(request, userId, cancellationToken);
public async Task<Result> UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateIssueAsync(request, userId, cancellationToken);
public async Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateMilestoneAsync(request, userId, cancellationToken); await service.UpdateMilestoneAsync(request, userId, cancellationToken);
public async ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateOutcomeAsync(request, userId, cancellationToken);
public async Task<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateProgrammeAsync(request, userId, cancellationToken); await service.UpdateProgrammeAsync(request, userId, cancellationToken);
public async ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateProjectAsync(request, userId, cancellationToken); await service.UpdateProjectAsync(request, userId, cancellationToken);
public async ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateProjectStatusAsync(request, userId, cancellationToken); await service.UpdateProjectStatusAsync(request, userId, cancellationToken);
public async Task<Result> UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.UpdateSiteVisitAsync(request, userId, cancellationToken);
} }
@@ -1,6 +1,7 @@
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared; using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
@@ -8,10 +9,11 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Projects; namespace PostFundManagement.Application.Services.Projects;
public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IProjectService public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
ITenantContextService tenantContext) : IProjectService
{ {
public async ValueTask<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -28,8 +30,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled."));
})); }));
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () => await Result.Try((Func<Task<Result<Project[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -41,8 +43,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(projects.Select(p => p.Map()).ToArray()); return Result.Ok(projects.Select(p => p.Map()).ToArray());
})); }));
public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project>>>)(async () => await Result.Try((Func<Task<Result<Project>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
@@ -59,8 +61,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.ToResult(project!.Map()); .ToResult(project!.Map());
})); }));
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity[]>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Activity[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
@@ -77,8 +79,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(activities.Select(a => a.Map()).ToArray()); return Result.Ok(activities.Select(a => a.Map()).ToArray());
})); }));
public async ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -101,15 +103,16 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
ProjectType = request.ProjectType, ProjectType = request.ProjectType,
Status = ApprovalStatus.Pending, Status = ApprovalStatus.Pending,
CreatedBy = userId, CreatedBy = userId,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow,
TenantId = tenantContext.TenantId
}); });
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0,
CreateError.Create("No changes detected, update cancelled.")); CreateError.Create("No changes detected, update cancelled."));
})); }));
public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -138,8 +141,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
CreateError.Create("No record was created.")); CreateError.Create("No record was created."));
})); }));
public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () => await Result.Try((Func<Task<Result<Milestone[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
@@ -157,8 +160,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(milestones.Select(i => i.Map()).ToArray()); return Result.Ok(milestones.Select(i => i.Map()).ToArray());
})); }));
public async ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) => public async Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () => await Result.Try((Func<Task<Result<Indicator[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
@@ -176,8 +179,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(evidences.Select(e => e.Map()).ToArray()); return Result.Ok(evidences.Select(e => e.Map()).ToArray());
})); }));
public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone>>>)(async () => await Result.Try((Func<Task<Result<Milestone>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash);
@@ -193,8 +196,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.ToResult(milestone!.Map()); .ToResult(milestone!.Map());
})); }));
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () => await Result.Try((Func<Task<Result<Milestone[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -207,8 +210,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(milestones.Select(i => i.Map()).ToArray()); return Result.Ok(milestones.Select(i => i.Map()).ToArray());
})); }));
public async ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -227,8 +230,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled."));
})); }));
public async ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -246,8 +249,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled."));
})); }));
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity>>>)(async () => await Result.Try((Func<Task<Result<Domain.Models.Activity>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(activityIdHash); var decodeResult = hashService.DecodeLongIdHash(activityIdHash);
@@ -264,8 +267,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.ToResult(activity!.Map()); .ToResult(activity!.Map());
})); }));
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () => await Result.Try((Func<Task<Result<Risk[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(projectIdHash); var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
@@ -283,8 +286,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(risks.Select(i => i.Map()).ToArray()); return Result.Ok(risks.Select(i => i.Map()).ToArray());
})); }));
public async ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -303,14 +306,15 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
Name = name, Name = name,
CreatedBy = userId, CreatedBy = userId,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
Status = ApprovalStatus.Pending Status = ApprovalStatus.Pending,
TenantId = tenantContext.TenantId
}); });
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -328,8 +332,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled."));
})); }));
public async ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme>>>)(async () => await Result.Try((Func<Task<Result<Programme>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(programmeIdHash); var decodeResult = hashService.DecodeLongIdHash(programmeIdHash);
@@ -344,8 +348,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.ToResult(programme!.Map()); .ToResult(programme!.Map());
})); }));
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) => public async Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () => await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -358,8 +362,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(programmes.Select(i => i.Map()).ToArray()); return Result.Ok(programmes.Select(i => i.Map()).ToArray());
})); }));
public async ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) => public async Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () => await Result.Try((Func<Task<Result<Project[]>>>)(async () =>
{ {
var decodeResult = hashService.DecodeLongIdHash(programmeIdHash); var decodeResult = hashService.DecodeLongIdHash(programmeIdHash);
@@ -378,4 +382,444 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
return Result.Ok(projects.Select(i => i.Map()).ToArray()); return Result.Ok(projects.Select(i => i.Map()).ToArray());
})); }));
public async Task<Result> CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Result.Fail(ValidationError.Create("Project does not exist"));
context.ChangeRequests.Add(new Domain.Entities.ChangeRequest
{
ProjectId = request.ProjectId,
Title = request.Title,
Justification = request.Justification,
RevisedBudget = request.RevisedBudget,
RevisedEndedAt = request.RevisionDate,
Status = ApprovalStatus.Pending,
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
TenantId = tenantContext.TenantId,
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0,
CreateError.Create("Failed to create change request"));
}));
public async Task<Result> UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.ChangeRequests
.Where(c => c.Id == request.ChangeRequestId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Status, request.Status)
.SetProperty(u => u.RevisedBudget, request.RevisedBudget)
.SetProperty(u => u.RevisedEndedAt, DateTime.UtcNow)
.SetProperty(u => u.Title, request.Title.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.Justification, request.Justification), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records were changed"));
}));
public async Task<Result<ChangeRequest[]>> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ChangeRequest[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<ChangeRequest[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changeRequests = await context.ChangeRequests.AsNoTracking()
.Where(c => c.ProjectId == decodeResult.Value)
.ToListAsync(cancellationToken);
return Result.Ok(changeRequests?.Select(i => i.Map()).ToArray() ?? []);
}));
public async Task<Result<ChangeRequest>> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ChangeRequest>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(changeRequestIdHash);
if (decodeResult.IsFailed)
return Result.Fail<ChangeRequest>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changeRequest = await context.ChangeRequests.AsNoTracking().FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(changeRequest is null, NotFoundError.New)
.ToResult(changeRequest!.Map());
}));
public async Task<Result> CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken))
return Result.Fail(ValidationError.Create("Project does not exist"));
context.ComplianceItems.Add(new Domain.Entities.ComplianceItem
{
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
ProjectId = request.ProjectId,
Requirements = request.Requirements,
Status = ApprovalStatus.Pending,
TenantId = tenantContext.TenantId,
Title = request.Title.Humanize(LetterCasing.Title).Trim()
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create compliance item"));
}));
public async Task<Result> UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.ComplianceItems
.Where(f => f.Id == request.ComplianceItemId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Title, request.Title.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.Requirements, request.Requirements)
.SetProperty(u => u.Status, request.Status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed"));
}));
public async Task<Result<ComplianceItem[]>> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ComplianceItem[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<ComplianceItem[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var complianceItems = await context.ComplianceItems.AsNoTracking()
.Where(c => c.ProjectId == decodeResult.Value)
.OrderByDescending(o => o.Id)
.ToListAsync(cancellationToken);
return Result.Ok(complianceItems?.Select(i => i.Map()).ToArray() ?? []);
}));
public async Task<Result<ComplianceItem>> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<ComplianceItem>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(complianceItemIdHash);
if (decodeResult.IsFailed)
return Result.Fail<ComplianceItem>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var compliancItem = await context.ComplianceItems.AsNoTracking().FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(compliancItem is null, NotFoundError.New)
.ToResult(compliancItem!.Map());
}));
public async Task<Result> CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
context.Beneficiaries.Add(new Domain.Entities.Beneficiary
{
Category = request.Category,
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
Location = request.Location,
Name = request.Name.Humanize(LetterCasing.Title).Trim(),
ProjectId = request.ProjectId,
ReachedCount = request.ReachedCount,
TargetCount = request.TargetCount,
UserId = userId,
TenantId = tenantContext.TenantId
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0,
CreateError.Create("Failed to create beneficiary"));
}));
public async Task<Result> UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Beneficiaries
.Where(f => f.Id == request.BeneficiaryId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.ReachedCount, request.ReachedCount)
.SetProperty(u => u.TargetCount, request.TargetCount)
.SetProperty(u => u.Location, request.Location)
.SetProperty(u => u.Category, request.Category.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed"));
}));
public async Task<Result<Beneficiary[]>> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Beneficiary[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Beneficiary[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var beneficiaries = await context.Beneficiaries.AsNoTracking()
.Where(b => b.ProjectId == decodeResult.Value)
.OrderByDescending(o => o.Id)
.ToListAsync(cancellationToken);
return Result.Ok(beneficiaries?.Select(i => i.Map()).ToArray() ?? []);
}));
public async Task<Result<Beneficiary>> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Beneficiary>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(beneficiaryIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Beneficiary>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var beneficiary = await context.Beneficiaries.AsNoTracking()
.FirstOrDefaultAsync(b => b.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(beneficiary is null, NotFoundError.New)
.ToResult(beneficiary!.Map());
}));
public async Task<Result> CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
context.Issues.Add(new Domain.Entities.Issue
{
AssignedTo = request.AssignedTo,
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
Description = request.Description,
Priority = request.Priority,
ProjectId = request.ProjectId,
Status = ActivityStatus.Active,
TenantId = tenantContext.TenantId,
Title = request.Title.Humanize(LetterCasing.Title).Trim()
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create issue"));
}));
public async Task<Result> UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Issues
.Where(i => i.Id == request.IssueId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Title, request.Title.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.Description, request.Description)
.SetProperty(u => u.Resolution, request.Resolution)
.SetProperty(u => u.ResolvedAt, DateTime.UtcNow)
.SetProperty(u => u.Status, request.Status)
.SetProperty(u => u.AssignedTo, request.AssignedTo), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changes"));
}));
public async Task<Result<Issue[]>> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Issue[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Issue[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var issues = await context.Issues.AsNoTracking()
.Where(i => i.ProjectId == decodeResult.Value)
.OrderByDescending(o => o.Id)
.ToListAsync(cancellationToken);
return Result.Ok(issues?.Select(i => i.Map()).ToArray() ?? []);
}));
public async Task<Result<Issue>> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Issue>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(issueIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Issue>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var issue = await context.Issues.AsNoTracking().FirstOrDefaultAsync(i => i.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(issue is null, NotFoundError.New)
.ToResult(issue!.Map());
}));
public async Task<Result> CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
context.Outcomes.Add(new Domain.Entities.Outcome
{
BeneficiaryId = request.BeneficiaryId,
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
Description = request.Description,
Name = request.Name.Humanize(LetterCasing.Title).Trim(),
TenantId = tenantContext.TenantId
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create outcome"));
}));
public async Task<Result> UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Outcomes
.Where(o => o.Id == request.OutcomeId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Description, request.Description)
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim()), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed"));
}));
public async Task<Result<Outcome[]>> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Outcome[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(beneficiaryIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Outcome[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var outcomes = await context.Outcomes.AsNoTracking()
.Where(o => o.BeneficiaryId == decodeResult.Value)
.OrderByDescending(o => o.Id)
.ToListAsync(cancellationToken);
return Result.Ok(outcomes?.Select(o => o.Map()).ToArray() ?? []);
}));
public async Task<Result<Outcome>> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Outcome>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(outcomeIdHash);
if (decodeResult.IsFailed)
return Result.Fail<Outcome>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var outcome = await context.Outcomes.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(outcome is null, NotFoundError.New)
.ToResult(outcome!.Map());
}));
public async Task<Result> CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
context.SiteVisits.Add(new Domain.Entities.SiteVisit
{
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
InspectorName = request.InspectorName.Humanize(LetterCasing.Title).Trim(),
Findings = request.Findings,
VisitedAt = request.VisitedAt
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create site visit"));
}));
public async Task<Result> UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.SiteVisits
.Where(v => v.Id == request.SiteVisitId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.InspectorName, request.InspectorName.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.Findings, request.Findings)
.SetProperty(u => u.Status, request.Status)
.SetProperty(u => u.VisitedAt, request.VisitedAt), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.Create("Failed to update site visit"));
}));
public async Task<Result<SiteVisit[]>> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<SiteVisit[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
if (decodeResult.IsFailed)
return Result.Fail<SiteVisit[]>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var visits = await context.SiteVisits.AsNoTracking()
.Where(v => v.ProjectId == decodeResult.Value)
.OrderByDescending(o => o.Id)
.ToListAsync(cancellationToken);
return Result.Ok(visits?.Select(i => i.Map()).ToArray() ?? []);
}));
public async Task<Result<SiteVisit>> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<SiteVisit>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(siteVisitIdHash);
if (decodeResult.IsFailed)
return Result.Fail<SiteVisit>(IdDecodeError.Create()
.CausedBy(decodeResult.Errors));
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var visit = await context.SiteVisits.AsNoTracking().FirstOrDefaultAsync(v => v.Id == decodeResult.Value, cancellationToken);
return Result.FailIf(visit is null, NotFoundError.New)
.ToResult(visit!.Map());
}));
} }
@@ -3,6 +3,75 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Projects; namespace PostFundManagement.Application.Services.Projects;
public sealed record UpdateSiteVisitRequest([Required] long SiteVisitId,
[Required][MaxLength(LabelLength)] string InspectorName,
[Required][MaxLength(TextLength)] string Findings,
[Required] ApprovalStatus Status,
[Required] DateTime VisitedAt);
public sealed record CreateSiteVisitRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string InspectorName,
[Required][MaxLength(TextLength)] string Findings,
[Required] DateTime VisitedAt);
public sealed record UpdateOutcomeRequest([Required] long OutcomeId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description);
public sealed record CreateOutcomeRequest([Required] long BeneficiaryId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description);
public sealed record UpdateIssueRequest([Required] long IssueId,
[Required][MaxLength(LabelLength)] string Title,
[Required][MaxLength(TextLength)] string Description,
[Required][MaxLength(TextLength)] string Resolution,
[Required] DateTime? ResolvedAt,
[Required] Priority Priority,
[Required] ActivityStatus Status,
[Required] Guid AssignedTo);
public sealed record CreateIssueRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Title,
[Required][MaxLength(TextLength)] string Description,
[Required] Priority Priority,
[Required] Guid AssignedTo);
public sealed record UpdateBeneficiaryRequest([Required] long BeneficiaryId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string Category,
[Required][MaxLength(LabelLength)] string Location,
[Required] int TargetCount,
[Required] int ReachedCount);
public sealed record CreateBeneficiaryRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string Category,
[Required][MaxLength(LabelLength)] string Location,
[Required] int TargetCount,
[Required] int ReachedCount);
public sealed record UpdateComplianceItemRequest([Required] long ComplianceItemId,
[Required][MaxLength(LabelLength)] string Title,
[Required][MaxLength(TextLength)] string Requirements,
[Required] ApprovalStatus Status);
public sealed record CreateComplianceItemRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Title,
[Required][MaxLength(TextLength)] string Requirements);
public sealed record UpdateChangeRequest([Required] long ChangeRequestId,
[Required][MaxLength(LabelLength)] string Title,
[Required][MaxLength(TextLength)] string Justification,
[Required] decimal RevisedBudget,
[Required] ApprovalStatus Status);
public sealed record CreateChangeRequest([Required] long ProjectId,
[Required][MaxLength(LabelLength)] string Title,
[Required][MaxLength(TextLength)] string Justification,
[Required] decimal RevisedBudget,
[Required] DateTime RevisionDate);
public sealed record UpdateProgrammeRequest([Required] long ProgrammeId, public sealed record UpdateProgrammeRequest([Required] long ProgrammeId,
[Required][MaxLength(LabelLength)] string Name, [Required][MaxLength(LabelLength)] string Name,
[Required] Guid OwnerId, [Required] Guid OwnerId,
@@ -0,0 +1,60 @@
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Shared;
public sealed class AuditService(IDbContextFactory<ApplicationDbContext> contextFactory, ITenantContextService tenantContextService) : IService
{
public async Task<Result<AuditLog[]>> GetAuditLogsAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<AuditLog[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var pagination = new Pagination { Page = 1, PageSize = 500, Index = 1 };
var logs = await context.AuditLogs
.OrderByDescending(o => o.Id)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(logs.Select(p => p.Map()).ToArray());
}));
public async Task<Result<AuditLog[]>> GetAuditLogsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<AuditLog[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var logs = await context.AuditLogs
.OrderByDescending(o => o.Id)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToListAsync(cancellationToken);
return Result.Ok(logs.Select(p => p.Map()).ToArray());
}));
public async Task<Result> LogActionAsync(string entityId, string entityName, string action, string[] changes, Guid performedBy, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
context.Add(new Domain.Entities.AuditLog
{
EntityId = entityId,
Action = action,
Changes = changes,
EntityName = entityName,
PerformedBy = performedBy,
Timestamp = DateTime.UtcNow,
TenantId = tenantContextService.TenantId > 0 ? tenantContextService.TenantId : null,
});
return Result.FailIf(await context.SaveChangesAsync(cancellationToken) <= 0, CreateError.New);
}));
}
@@ -4,32 +4,32 @@ namespace PostFundManagement.Application.Services.Shared;
public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage) public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
{ {
public async ValueTask<Result> DeleteAsync(string key) => public async Task<Result> DeleteAsync(string key) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await storage.DeleteAsync(key); await storage.DeleteAsync(key);
return Result.Ok(); return Result.Ok();
})); }));
public async ValueTask<Result> SaveAsync(string key, string value) => public async Task<Result> SaveAsync(string key, string value) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await storage.SetAsync(key, value); await storage.SetAsync(key, value);
return Result.Ok(); return Result.Ok();
})); }));
public async ValueTask<Result> SaveAsync<TValue>(string key, TValue value) where TValue : class => public async Task<Result> SaveAsync<TValue>(string key, TValue value) where TValue : class =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await storage.SetAsync(key, value); await storage.SetAsync(key, value);
return Result.Ok(); return Result.Ok();
})); }));
public async ValueTask<Result<string>> GetAsync(string key) => public async Task<Result<string>> GetAsync(string key) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () => await Result.Try((Func<Task<Result<string>>>)(async () =>
{ {
var retrieval = await storage.GetAsync<string>(key); var retrieval = await storage.GetAsync<string>(key);
@@ -38,8 +38,8 @@ public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
.ToResult(retrieval.Value!); .ToResult(retrieval.Value!);
})); }));
public async ValueTask<Result<TValue>> GetAsync<TValue>(string key) where TValue : class => public async Task<Result<TValue>> GetAsync<TValue>(string key) where TValue : class =>
await Result.Try((Func<ValueTask<Result<TValue>>>)(async () => await Result.Try((Func<Task<Result<TValue>>>)(async () =>
{ {
var retrieval = await storage.GetAsync<TValue>(key); var retrieval = await storage.GetAsync<TValue>(key);
@@ -17,7 +17,7 @@ public sealed class EmailService(IOptions<SmtpSettings> options) : IDisposable
public EmailStatuses Status { get; private set; } = EmailStatuses.Disconnected; public EmailStatuses Status { get; private set; } = EmailStatuses.Disconnected;
public async ValueTask<Result<Response>> SendEmailAsync(Message message, CancellationToken cancellationToken = default) public async Task<Result<Response>> SendEmailAsync(Message message, CancellationToken cancellationToken = default)
{ {
using var activity = EmailTelemetry.Source.StartActivity("Email Send"); using var activity = EmailTelemetry.Source.StartActivity("Email Send");
@@ -121,7 +121,7 @@ public sealed class EmailService(IOptions<SmtpSettings> options) : IDisposable
return Result.Fail<Response>(response); return Result.Fail<Response>(response);
} }
public async ValueTask<Result<Response>> ConnectAsync(CancellationToken cancellationToken = default) public async Task<Result<Response>> ConnectAsync(CancellationToken cancellationToken = default)
{ {
using var activity = EmailTelemetry.Source.StartActivity("Email Connect"); using var activity = EmailTelemetry.Source.StartActivity("Email Connect");
activity?.SetTag("email.smtp.connect", settings.Host); activity?.SetTag("email.smtp.connect", settings.Host);
@@ -174,7 +174,7 @@ public sealed class EmailService(IOptions<SmtpSettings> options) : IDisposable
} }
} }
public async ValueTask<Result> DisconnectAsync(CancellationToken cancellationToken = default) public async Task<Result> DisconnectAsync(CancellationToken cancellationToken = default)
{ {
using var activity = EmailTelemetry.Source.StartActivity("Email Disconnect"); using var activity = EmailTelemetry.Source.StartActivity("Email Disconnect");
activity?.SetTag("email.smtp.disconnect", settings.Host); activity?.SetTag("email.smtp.disconnect", settings.Host);
@@ -67,7 +67,7 @@ public sealed partial class HashService(IHashids hasher) : IService
} }
public async Task<Result<string>> DecodeHexHash(string hex) => public async Task<Result<string>> DecodeHexHash(string hex) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () => await Result.Try((Func<Task<Result<string>>>)(async () =>
{ {
string decoded = hasher.DecodeHex(hex); string decoded = hasher.DecodeHex(hex);
@@ -5,6 +5,6 @@ namespace PostFundManagement.Application.Services.Shared;
public sealed class IdentityCacheService(IdentityService service) : ServiceCacheBase, IIdentityService public sealed class IdentityCacheService(IdentityService service) : ServiceCacheBase, IIdentityService
{ {
public async ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) =>
await service.UserIsAuthorisedAsync(userId, cancellationToken); await service.UserIsAuthorisedAsync(userId, cancellationToken);
} }
@@ -7,11 +7,12 @@ namespace PostFundManagement.Application.Services.Shared;
public sealed class IdentityService(IDbContextFactory<ApplicationDbContext> contextFactory) : IIdentityService public sealed class IdentityService(IDbContextFactory<ApplicationDbContext> contextFactory) : IIdentityService
{ {
public async ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) => public async Task<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
// TODO: Ensure TenantUser status is checked
var userExistsAndActive = await context.Users var userExistsAndActive = await context.Users
.AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken); .AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken);
@@ -9,8 +9,8 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
{ {
private readonly SecurityClientSettings clientSettings = clientOptions.Value; private readonly SecurityClientSettings clientSettings = clientOptions.Value;
public async ValueTask<Result<Domain.Security.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default) => public async Task<Result<Domain.Security.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Security.Models.TokenResponse>>>)(async () => await Result.Try((Func<Task<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
{ {
using var response = await connectApi.GetToken(new Domain.Security.Models.TokenRequest using var response = await connectApi.GetToken(new Domain.Security.Models.TokenRequest
{ {
@@ -34,7 +34,7 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
.ToResult(tokenResponse!); .ToResult(tokenResponse!);
} }
return await Result.Try((Func<ValueTask<Result<Domain.Security.Models.TokenResponse>>>)(async () => return await Result.Try((Func<Task<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
{ {
var errorResult = JsonSerializer.Deserialize<TokenErrorResponse>(contentRaw); var errorResult = JsonSerializer.Deserialize<TokenErrorResponse>(contentRaw);
@@ -1,34 +0,0 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Tools;
public sealed class ToolCacheService(ToolService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IToolService
{
public async ValueTask<Result<string>> EncodeLong(long id, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.EncodeLong(id, cancellationToken);
var cacheKey = HashService.ToMd5Hash($"encodelonghash-{id}").Value;
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
if(string.IsNullOrWhiteSpace(cached))
{
var result = (await service.EncodeLong(id).ConfigureAwait(false));
if (result.IsFailed) return result;
await cache.SetStringAsync(cacheKey, result.Value, cacheOptions, cancellationToken);
return result.Value;
}
return Result.Ok(cached);
}));
}
@@ -1,10 +0,0 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Services.Shared;
namespace PostFundManagement.Application.Services.Tools;
public sealed class ToolService(HashService service) : IToolService
{
public ValueTask<Result<string>> EncodeLong(long id, CancellationToken cancellationToken = default) =>
ValueTask.FromResult(service.HashEncodeLongId(id));
}
@@ -11,8 +11,8 @@ namespace PostFundManagement.Application.Services.Users;
public sealed class UserCacheService(UserService service, IDistributedCache cache, public sealed class UserCacheService(UserService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IUserService IFeatureManager featureManager) : ServiceCacheBase, IUserService
{ {
public async ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) => public async Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<User>>>)(async () => await Result.Try((Func<Task<Result<User>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetUserAsync(userId, cancellationToken); return await service.GetUserAsync(userId, cancellationToken);
@@ -35,8 +35,8 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach
return Result.Ok(cached.FromJson<User>()); return Result.Ok(cached.FromJson<User>());
})); }));
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) => public async Task<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<User[]>>>)(async () => await Result.Try((Func<Task<Result<User[]>>>)(async () =>
{ {
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetUsersAsync(pagination, request, cancellationToken); return await service.GetUsersAsync(pagination, request, cancellationToken);
@@ -61,9 +61,9 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach
return Result.Ok(cached.FromJson<User[]>()); return Result.Ok(cached.FromJson<User[]>());
})); }));
public async ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default) => public async Task<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default) =>
await service.SyncUserAsync(userId, email, cancellationToken); await service.SyncUserAsync(userId, email, cancellationToken);
public async ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) => public async Task<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) =>
await service.UpdateUserStatusAsync(userId, status, cancellationToken); await service.UpdateUserStatusAsync(userId, status, cancellationToken);
} }
@@ -1,5 +1,6 @@
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Domain; using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Models;
@@ -7,16 +8,19 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Users; namespace PostFundManagement.Application.Services.Users;
public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextFactory) : IUserService public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextFactory, ITenantContextService tenantContext) : IUserService
{ {
public async ValueTask<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default) => public async Task<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); var tenantUser = await context.TenantUsers
.Include(i => i.User)
.FirstOrDefaultAsync(u => u.UserId == userId, cancellationToken);
if (user is null) if (tenantUser is null)
{
context.Users.Add(new Domain.Entities.User context.Users.Add(new Domain.Entities.User
{ {
Id = userId, Id = userId,
@@ -24,35 +28,45 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
Status = ActivityStatus.Active, Status = ActivityStatus.Active,
LastLoginAt = DateTime.UtcNow LastLoginAt = DateTime.UtcNow
}); });
context.TenantUsers.Add(new Domain.Entities.TenantUser
{
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
Status = ActivityStatus.Active,
TenantId = tenantContext.TenantId,
UserId = userId
});
}
else else
{ {
user!.Email = email; tenantUser.User!.Email = email;
user.LastLoginAt = DateTime.UtcNow; tenantUser.User.LastLoginAt = DateTime.UtcNow;
context.Users.Update(user); context.Users.Update(tenantUser.User);
} }
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
})); }));
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) => public async Task<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<User[]>>>)(async () => await Result.Try((Func<Task<Result<User[]>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Users.AsNoTracking(); var query = context.TenantUsers.Include(i => i.User).AsNoTracking();
if (request.Status.HasValue) if (request.Status.HasValue)
query = query.Where(u => u.Status == request.Status.Value); query = query.Where(u => u.Status == request.Status.Value);
if (!string.IsNullOrWhiteSpace(request.Email)) if (!string.IsNullOrWhiteSpace(request.Email))
query = query.Where(u => u.Email != null && u.Email.Contains(request.Email)); query = query.Where(u => u.User!.Email != null && u.User!.Email.Contains(request.Email));
if (!string.IsNullOrWhiteSpace(request.SortBy)) if (!string.IsNullOrWhiteSpace(request.SortBy))
query = request.SortBy.ToLowerInvariant() switch query = request.SortBy.ToLowerInvariant() switch
{ {
"email" => request.SortDescending ? query.OrderByDescending(u => u.Email) : query.OrderBy(u => u.Email), "email" => request.SortDescending ? query.OrderByDescending(u => u.User!.Email) : query.OrderBy(u => u.User!.Email),
"lastloginat" => request.SortDescending ? query.OrderByDescending(u => u.LastLoginAt) : query.OrderBy(u => u.LastLoginAt), "lastloginat" => request.SortDescending ? query.OrderByDescending(u => u.User!.LastLoginAt) : query.OrderBy(u => u.User!.LastLoginAt),
"status" => request.SortDescending ? query.OrderByDescending(u => u.Status) : query.OrderBy(u => u.Status), "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) _ => request.SortDescending ? query.OrderByDescending(u => u.Id) : query.OrderBy(u => u.Id)
}; };
@@ -62,30 +76,36 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
var users = await query.Skip((pagination.Page - 1) * pagination.PageSize) var users = await query.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize).ToListAsync(cancellationToken); .Take(pagination.PageSize).ToListAsync(cancellationToken);
return Result.Ok(users.Select(u => u.Map()).ToArray()); return Result.Ok(users.Select(u => u.User!.Map()).ToArray());
})); }));
public async ValueTask<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) => public async Task<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () => await Result.Try((Func<Task<Result>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Users var changedTotal = await context.TenantUsers
.Where(f => f.Id == userId) .Include(i => i.User)
.Where(f => f.UserId == userId)
.ExecuteUpdateAsync(f => f
.SetProperty(u => u.Status, status), cancellationToken);
changedTotal += await context.Users.Where(f => f.Id == userId)
.ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken); .ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, status), cancellationToken);
return Result.OkIf(changedTotal > 0, UpdateError.New); return Result.OkIf(changedTotal > 0, UpdateError.New);
})); }));
public async ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) => public async Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<User>>>)(async () => await Result.Try((Func<Task<Result<User>>>)(async () =>
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var user = await context.Users.AsNoTracking() var tenantUser = await context.TenantUsers.AsNoTracking()
.FirstOrDefaultAsync(f => f.Id == userId, cancellationToken); .Include(i => i.User)
.FirstOrDefaultAsync(f => f.UserId == userId, cancellationToken);
return Result.FailIf(user is null, NotFoundError.New) return Result.FailIf(tenantUser == null, NotFoundError.New)
.ToResult(user!.Map()); .ToResult(tenantUser!.User?.Map()!);
})); }));
} }
File diff suppressed because it is too large Load Diff
+73 -3
View File
@@ -9,11 +9,14 @@
"@hookform/resolvers": "^5.9.1", "@hookform/resolvers": "^5.9.1",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@tanstack/react-query": "^5.102.8", "@tanstack/react-query": "^5.102.8",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
"axios": "^1.20.0", "axios": "^1.20.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^4.21.2", "express": "^4.21.2",
"hashids": "^2.3.0", "hashids": "^2.3.0",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8",
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"motion": "^12.23.24", "motion": "^12.23.24",
"oidc-client-ts": "^3.5.0", "oidc-client-ts": "^3.5.0",
@@ -23,14 +26,15 @@
"react-router-dom": "^7.18.3", "react-router-dom": "^7.18.3",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"vite": "^6.2.3", "vite": "^6.2.3",
"zod": "^4.5.4", "xlsx": "^0.18.5",
"zod": "^3.25.76",
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"postcss": "^8.5.28",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "~5.8.2", "typescript": "~5.8.2",
}, },
@@ -69,6 +73,8 @@
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
@@ -273,8 +279,12 @@
"@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="],
"@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="],
"@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
"@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
@@ -283,12 +293,16 @@
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@2.3.0", "", { "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ=="], "@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@2.3.0", "", { "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="],
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
@@ -299,6 +313,8 @@
"axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="], "axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="],
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.11.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ=="],
@@ -319,6 +335,12 @@
"caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
"canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="],
"cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="],
"codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
@@ -331,6 +353,12 @@
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
"core-js": ["core-js@3.50.0", "", {}, "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw=="],
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
"css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -343,6 +371,8 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dompurify": ["dompurify@3.4.16", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -377,10 +407,14 @@
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-png": ["fast-png@6.4.0", "", { "dependencies": { "@types/pako": "^2.0.3", "iobuffer": "^5.3.2", "pako": "^2.1.0" } }, "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
"follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
@@ -391,6 +425,8 @@
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
"framer-motion": ["framer-motion@12.43.0", "", { "dependencies": { "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g=="], "framer-motion": ["framer-motion@12.43.0", "", { "dependencies": { "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g=="],
@@ -427,6 +463,8 @@
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
@@ -435,6 +473,8 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"iobuffer": ["iobuffer@5.4.0", "", {}, "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
@@ -447,6 +487,10 @@
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jspdf": ["jspdf@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.3.1", "html2canvas": "^1.0.0-rc.5" } }, "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ=="],
"jspdf-autotable": ["jspdf-autotable@5.0.8", "", { "peerDependencies": { "jspdf": "^2 || ^3 || ^4" } }, "sha512-Hy05N86yBO7CXBrnSLOge7i1ZYpKH2DjQ94iybaP7vBhSInjvRBgDc99ngKzSbSO8Jc98ZCally8I6n0tj2RJQ=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
@@ -525,10 +569,14 @@
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"pako": ["pako@2.2.0", "", {}, "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], "path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
@@ -545,6 +593,8 @@
"qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
"raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
@@ -561,8 +611,12 @@
"react-router-dom": ["react-router-dom@7.18.3", "", { "dependencies": { "react-router": "7.18.3" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg=="], "react-router-dom": ["react-router-dom@7.18.3", "", { "dependencies": { "react-router": "7.18.3" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg=="],
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="],
"rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="], "rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@@ -591,12 +645,20 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="],
"stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"svg-pathdata": ["svg-pathdata@6.0.3", "", {}, "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="],
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
@@ -617,17 +679,25 @@
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="],
"word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="],
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
"xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -23,6 +23,8 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^4.21.2", "express": "^4.21.2",
"hashids": "^2.3.0", "hashids": "^2.3.0",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8",
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"motion": "^12.23.24", "motion": "^12.23.24",
"oidc-client-ts": "^3.5.0", "oidc-client-ts": "^3.5.0",
@@ -32,6 +34,7 @@
"react-router-dom": "^7.18.3", "react-router-dom": "^7.18.3",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"vite": "^6.2.3", "vite": "^6.2.3",
"xlsx": "^0.18.5",
"zod": "^3.25.76" "zod": "^3.25.76"
}, },
"devDependencies": { "devDependencies": {
@@ -39,6 +42,7 @@
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"postcss": "^8.5.28",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "~5.8.2" "typescript": "~5.8.2"
} }
+9
View File
@@ -10,6 +10,9 @@ const LandingPage = lazy(() => import('./pages/LandingPage').then(m => ({ defaul
const ExecutiveDashboard = lazy(() => import('./pages/ExecutiveDashboard').then(m => ({ default: m.ExecutiveDashboard }))); const ExecutiveDashboard = lazy(() => import('./pages/ExecutiveDashboard').then(m => ({ default: m.ExecutiveDashboard })));
const GovernancePage = lazy(() => import('./pages/GovernancePage').then(m => ({ default: m.GovernancePage }))); const GovernancePage = lazy(() => import('./pages/GovernancePage').then(m => ({ default: m.GovernancePage })));
const ProjectsPage = lazy(() => import('./pages/ProjectsPage').then(m => ({ default: m.ProjectsPage }))); const ProjectsPage = lazy(() => import('./pages/ProjectsPage').then(m => ({ default: m.ProjectsPage })));
const BeneficiariesPage = lazy(() => import('./pages/BeneficiariesPage').then(m => ({ default: m.BeneficiariesPage })));
const ChangeManagementPage = lazy(() => import('./pages/ChangeManagementPage').then(m => ({ default: m.ChangeManagementPage })));
const SiteVisitsPage = lazy(() => import('./pages/SiteVisitsPage').then(m => ({ default: m.SiteVisitsPage })));
const GrantsPage = lazy(() => import('./pages/GrantsPage').then(m => ({ default: m.GrantsPage }))); const GrantsPage = lazy(() => import('./pages/GrantsPage').then(m => ({ default: m.GrantsPage })));
const MonitoringPage = lazy(() => import('./pages/MonitoringPage').then(m => ({ default: m.MonitoringPage }))); const MonitoringPage = lazy(() => import('./pages/MonitoringPage').then(m => ({ default: m.MonitoringPage })));
const SettingsPage = lazy(() => import('./pages/SettingsPage').then(m => ({ default: m.SettingsPage }))); const SettingsPage = lazy(() => import('./pages/SettingsPage').then(m => ({ default: m.SettingsPage })));
@@ -46,6 +49,12 @@ export default function App() {
<Route path="/dashboard" element={<ExecutiveDashboard />} /> <Route path="/dashboard" element={<ExecutiveDashboard />} />
<Route path="/governance" element={<GovernancePage />} /> <Route path="/governance" element={<GovernancePage />} />
<Route path="/projects" element={<ProjectsPage />} /> <Route path="/projects" element={<ProjectsPage />} />
<Route path="/projects/beneficiaries" element={<BeneficiariesPage />} />
<Route path="/beneficiaries" element={<BeneficiariesPage />} />
<Route path="/projects/change-management" element={<ChangeManagementPage />} />
<Route path="/change-management" element={<ChangeManagementPage />} />
<Route path="/projects/site-visits" element={<SiteVisitsPage />} />
<Route path="/site-visits" element={<SiteVisitsPage />} />
<Route path="/grants" element={<GrantsPage />} /> <Route path="/grants" element={<GrantsPage />} />
<Route path="/monitoring" element={<MonitoringPage />} /> <Route path="/monitoring" element={<MonitoringPage />} />
<Route path="/settings" element={<SettingsPage />} /> <Route path="/settings" element={<SettingsPage />} />

Some files were not shown because too many files have changed in this diff Show More