110 Commits
Author SHA1 Message Date
khwezi a7a68fc290 Merge pull request 'Finishline' (#92) from finishline into main
Reviewed-on: #92
2026-09-25 19:06:37 +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
khwezi 4dbc74e488 Merge pull request 'Finishline' (#91) from finishline into main
Reviewed-on: #91
2026-09-24 16:08:03 +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
khwezi d658497545 Merge pull request 'Finishline' (#90) from finishline into main
Reviewed-on: #90
2026-09-24 12:01:36 +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
khwezi b947959724 Merge pull request 'Added linkage to primary tenant entities' (#89) from finishline into main
Reviewed-on: #89
2026-09-22 07:09:20 +02:00
hermes 717b891554 Added linkage to primary tenant entities 2026-09-22 07:08:57 +02:00
khwezi 58126e7573 Merge pull request 'Make retrieval of contract file auditable' (#88) from finishline into main
Reviewed-on: #88
2026-09-22 06:29:58 +02:00
hermes 91d325ce4e Make retrieval of contract file auditable 2026-09-22 06:28:54 +02:00
khwezi 2fede443cc Merge pull request 'Added automatic audit feature' (#87) from finishline into main
Reviewed-on: #87
2026-09-22 06:11:18 +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
khwezi 80f220fcae Merge pull request 'Test 2 reoslutions' (#86) from garages3 into main
Reviewed-on: #86
2026-09-20 15:26:35 +02:00
khwezi 06e2c3ee6a Merge pull request 'Remediated test #1 results' (#85) from garages3 into main
Reviewed-on: #85
2026-09-20 13:52:03 +02:00
khwezi 3c0fd8844c Merge pull request 'Refactored UI endpoint usage' (#84) from garages3 into main
Reviewed-on: #84
2026-09-20 08:27:14 +02:00
khwezi b7c135f0fc Merge pull request 'Garages3' (#83) from garages3 into main
Reviewed-on: #83
2026-09-18 16:12:06 +02:00
khwezi db053e3e5f Merge pull request 'Garages3' (#82) from garages3 into main
Reviewed-on: #82
2026-09-17 07:39:34 +02:00
khwezi d7177e1a72 Merge pull request 'Removed package.json' (#81) from garages3 into main
Reviewed-on: #81
2026-09-17 07:23:54 +02:00
khwezi c67041faea Merge pull request 'Refactored UI and compose file' (#80) from garages3 into main
Reviewed-on: #80
2026-09-17 07:08:26 +02:00
khwezi 0e8a5e1169 Merge pull request 'Removed docker debug code' (#79) from garages3 into main
Reviewed-on: #79
2026-09-15 12:13:47 +02:00
khwezi 41fdb067fe Merge pull request 'Refactored ApiVersioning service registration' (#78) from garages3 into main
Reviewed-on: #78
2026-09-15 12:09:47 +02:00
khwezi b4b687dfcc Merge pull request 'Added OPTIONS header handling' (#77) from garages3 into main
Reviewed-on: #77
2026-09-15 08:52:23 +02:00
khwezi f200ccbf60 Merge pull request 'Added RequireCors() to RouteGroup' (#76) from garages3 into main
Reviewed-on: #76
2026-09-15 08:45:25 +02:00
khwezi 27f57150e1 Merge pull request 'Moved .RequireAuthentication() to endpoints, removed it from EndpointGroup' (#75) from garages3 into main
Reviewed-on: #75
2026-09-15 08:37:43 +02:00
khwezi 79c63107e9 Merge pull request 'Applied configurable CORS policy to API' (#74) from garages3 into main
Reviewed-on: #74
2026-09-15 07:30:29 +02:00
khwezi d945731252 Merge pull request 'Fixed UI' (#73) from garages3 into main
Reviewed-on: #73
2026-09-14 11:44:00 +02:00
khwezi c979a4632e Merge pull request 'Debug refactor' (#72) from garages3 into main
Reviewed-on: #72
2026-09-14 10:46:59 +02:00
khwezi 9eed3cff6b Merge pull request 'Debugs refactor' (#71) from garages3 into main
Reviewed-on: #71
2026-09-14 10:39:04 +02:00
khwezi 6db1a2f367 Merge pull request 'Garages3' (#70) from garages3 into main
Reviewed-on: #70
2026-09-14 10:34:46 +02:00
khwezi 06d9952cd7 Merge pull request 'Rebuilt pfm-api dockerfile' (#69) from garages3 into main
Reviewed-on: #69
2026-09-14 10:29:13 +02:00
khwezi 92d481f75d Merge pull request 'Light refactor' (#68) from garages3 into main
Reviewed-on: #68
2026-09-14 10:23:19 +02:00
khwezi 1c36602995 Merge pull request 'Simplified fpm-api dockerfile' (#67) from garages3 into main
Reviewed-on: #67
2026-09-14 10:21:10 +02:00
khwezi 5ae0c500ac Merge pull request 'Added debug code' (#66) from garages3 into main
Reviewed-on: #66
2026-09-14 10:13:58 +02:00
khwezi db04f800e9 Merge pull request 'Changed pfm-api dockerfile paths' (#65) from garages3 into main
Reviewed-on: #65
2026-09-14 10:10:49 +02:00
khwezi e7a19611f0 Merge pull request 'Simplified dockerfile (pfm-api)' (#64) from garages3 into main
Reviewed-on: #64
2026-09-14 10:04:10 +02:00
khwezi 5ad24da7cf Merge pull request 'Refactored compose file to assign same context to both services' (#63) from garages3 into main
Reviewed-on: #63
2026-09-14 10:02:01 +02:00
khwezi 156dae40b7 Merge pull request 'Fixed missing file copy' (#62) from garages3 into main
Reviewed-on: #62
2026-09-14 09:50:32 +02:00
khwezi 1b67d9cf8b Merge pull request 'Debug refactor on dockerfile' (#61) from garages3 into main
Reviewed-on: #61
2026-09-14 09:47:00 +02:00
khwezi 1bd05e64e5 Merge pull request 'Added better dotnet build handling' (#60) from garages3 into main
Reviewed-on: #60
2026-09-14 09:42:10 +02:00
khwezi 303dba3c6f Merge pull request 'Added more verbosity to dotnet build' (#59) from garages3 into main
Reviewed-on: #59
2026-09-14 09:35:30 +02:00
khwezi 1dd4f54a1d Merge pull request 'Added .dockerignore' (#58) from garages3 into main
Reviewed-on: #58
2026-09-14 09:32:21 +02:00
khwezi fae1e080c4 Merge pull request 'Added stream redirection to dotnet commands' (#57) from garages3 into main
Reviewed-on: #57
2026-09-14 09:14:17 +02:00
khwezi 4de23d27b3 Merge pull request 'Commented out pfm-console to see if the api image will build' (#56) from garages3 into main
Reviewed-on: #56
2026-09-14 09:06:50 +02:00
khwezi d4bce12dbb Merge pull request 'Removed stream redirection' (#55) from garages3 into main
Reviewed-on: #55
2026-09-13 18:15:33 +02:00
khwezi 299b876e1c Merge pull request 'Fixed Api docker file paths' (#54) from garages3 into main
Reviewed-on: #54
2026-09-13 18:13:13 +02:00
khwezi dde1dbe056 Merge pull request 'Added debug code to dotnet build and publish lines' (#53) from garages3 into main
Reviewed-on: #53
2026-09-13 18:09:51 +02:00
khwezi b81e6feb45 Merge pull request 'Moved api dockerfile to project folder' (#52) from garages3 into main
Reviewed-on: #52
2026-09-13 18:07:52 +02:00
khwezi 4138a1941f Merge pull request 'Corrected compose service contexts' (#51) from garages3 into main
Reviewed-on: #51
2026-09-13 18:01:52 +02:00
khwezi 17af31d810 Merge pull request 'Fixed dockerfile paths' (#50) from garages3 into main
Reviewed-on: #50
2026-09-13 17:56:19 +02:00
khwezi b85cf6a1cb Merge pull request 'Light refactor' (#49) from garages3 into main
Reviewed-on: #49
2026-09-13 17:51:45 +02:00
khwezi 81ddc11746 Merge pull request 'Refactored API dockerfile' (#48) from garages3 into main
Reviewed-on: #48
2026-09-13 17:50:29 +02:00
khwezi 5b94672ff5 Merge pull request 'Stable API run' (#47) from garages3 into main
Reviewed-on: #47
2026-09-13 17:41:42 +02:00
khwezi c535392ef1 Merge pull request 'Added debug term' (#46) from garages3 into main
Reviewed-on: #46
2026-09-12 12:22:43 +02:00
khwezi 0607de1802 Merge pull request 'Second fix' (#45) from garages3 into main
Reviewed-on: #45
2026-09-12 12:19:34 +02:00
khwezi 4c1e52e452 Merge pull request 'Light refactor' (#44) from garages3 into main
Reviewed-on: #44
2026-09-12 12:14:13 +02:00
khwezi 2414d74d1c Merge pull request 'Light refactor' (#43) from garages3 into main
Reviewed-on: #43
2026-09-12 12:10:17 +02:00
khwezi 93d9661a7f Merge pull request 'Rewrite pfm-api docker file' (#42) from garages3 into main
Reviewed-on: #42
2026-09-12 12:04:47 +02:00
khwezi dba401fe94 Merge pull request 'Fixed publish alias' (#41) from garages3 into main
Reviewed-on: #41
2026-09-12 11:58:37 +02:00
khwezi 42d73fe6cb Merge pull request 'Added error output to dotnet build line' (#40) from garages3 into main
Reviewed-on: #40
2026-09-12 11:54:57 +02:00
khwezi 1475887a7f Merge pull request 'Added working directory switch' (#39) from garages3 into main
Reviewed-on: #39
2026-09-12 11:49:52 +02:00
khwezi 730664d9c4 Merge pull request 'PFM Api docker refactor' (#38) from garages3 into main
Reviewed-on: #38
2026-09-12 11:45:32 +02:00
khwezi f38e569a34 Merge pull request 'Implemented missing file handling endpoints' (#37) from garages3 into main
Reviewed-on: #37
2026-09-12 11:26:45 +02:00
khwezi 7e79171f97 Merge pull request 'Added image name to compose file for console' (#36) from endpoints into main
Reviewed-on: #36
2026-09-11 16:24:11 +02:00
khwezi d7b868f2c1 Merge pull request 'Added more cors origins' (#35) from endpoints into main
Reviewed-on: #35
2026-09-11 16:17:07 +02:00
khwezi d43a09b371 Merge pull request 'Refactored console dockerfile' (#34) from endpoints into main
Reviewed-on: #34
2026-09-11 16:05:21 +02:00
khwezi 0dc0de1502 Merge pull request 'Fixed dockerfile paths' (#33) from endpoints into main
Reviewed-on: #33
2026-09-11 15:47:30 +02:00
khwezi de47dd4a4f Merge pull request 'Supplied defaults to compose env variables' (#32) from endpoints into main
Reviewed-on: #32
2026-09-11 15:33:47 +02:00
khwezi 2810cc669b Merge pull request 'Fixed compose syntax error' (#31) from endpoints into main
Reviewed-on: #31
2026-09-11 15:30:40 +02:00
khwezi 734af8c86f Merge pull request 'Refactored compose file to include the ui in deployment' (#30) from endpoints into main
Reviewed-on: #30
2026-09-11 15:29:21 +02:00
khwezi a754c42d14 Merge pull request 'Updated Console compose file to use variables' (#29) from endpoints into main
Reviewed-on: #29
2026-09-11 15:19:31 +02:00
khwezi bd7ca9372f Merge pull request 'Added specific origin' (#28) from endpoints into main
Reviewed-on: #28
2026-09-11 14:59:34 +02:00
khwezi 0d01d91ab4 Merge pull request 'Added cors after routes' (#27) from endpoints into main
Reviewed-on: #27
2026-09-11 14:54:47 +02:00
khwezi 2c817a8818 Merge pull request 'Endpoints' (#26) from endpoints into main
Reviewed-on: #26
2026-09-11 14:35:02 +02:00
khwezi 4c3a96917b Merge pull request 'Improved results handling in service layer' (#25) from endpoints into main
Reviewed-on: #25
2026-09-11 10:33:47 +02:00
khwezi e5c3a75795 Merge pull request 'Stable run with validations' (#24) from endpoints into main
Reviewed-on: #24
2026-09-06 10:49:57 +02:00
khwezi 1bc7e65960 Merge pull request 'Added tooling' (#23) from endpoints into main
Reviewed-on: #23
2026-09-05 18:27:35 +02:00
khwezi 094126d84b Merge pull request 'Fixed endpointname uniqueness' (#22) from endpoints into main
Reviewed-on: #22
2026-09-05 13:07:41 +02:00
khwezi 2506241a25 Merge pull request 'Fixed endpoint uniqueness' (#21) from endpoints into main
Reviewed-on: #21
2026-09-05 12:58:17 +02:00
khwezi cfe3530639 Merge pull request 'Endpoints' (#20) from endpoints into main
Reviewed-on: #20
2026-09-05 12:50:04 +02:00
khwezi 985557bee4 Merge pull request 'Implemented caching on all get endpoints and service layer' (#19) from endpoints into main
Reviewed-on: #19
2026-09-05 08:30:16 +02:00
khwezi f0b8b3051a Merge pull request 'Improved getusers cache key' (#18) from endpoints into main
Reviewed-on: #18
2026-09-04 06:08:39 +02:00
khwezi 6c88b9e533 Merge pull request 'Endpoints' (#17) from endpoints into main
Reviewed-on: #17
2026-09-04 05:54:30 +02:00
khwezi 7d67408d2c Merge pull request 'Additional redis configs' (#16) from endpoints into main
Reviewed-on: #16
2026-09-04 05:39:44 +02:00
khwezi d1d58a6183 Merge pull request 'Added service level caching to IdentityService, Communication and Evidence' (#15) from endpoints into main
Reviewed-on: #15
2026-09-03 16:38:08 +02:00
khwezi 60ad154841 Merge pull request 'Implemented distributed caching' (#14) from endpoints into main
Reviewed-on: #14
2026-09-03 11:09:52 +02:00
khwezi 2c13170859 Merge pull request 'Endpoints' (#13) from endpoints into main
Reviewed-on: #13
2026-09-02 16:14:34 +02:00
khwezi d87c8aa3c9 Merge pull request 'Updated configurations' (#12) from endpoints into main
Reviewed-on: #12
2026-09-02 09:45:38 +02:00
khwezi 6af7815ad1 Merge pull request 'Removed node tooling install' (#11) from endpoints into main
Reviewed-on: #11
2026-09-02 09:30:06 +02:00
khwezi a169f6c3f6 Merge pull request 'Endpoints' (#10) from endpoints into main
Reviewed-on: #10
2026-09-02 09:28:17 +02:00
khwezi 65ea753c19 Merge pull request 'Endpoints' (#9) from endpoints into main
Reviewed-on: #9
2026-09-01 12:29:47 +02:00
khwezi e6d2663265 Merge pull request 'Endpoints' (#8) from endpoints into main
Reviewed-on: #8
2026-08-30 17:47:24 +02:00
khwezi 91b5814b60 Merge pull request 'Endpoints' (#7) from endpoints into main
Reviewed-on: #7
2026-08-30 16:27:37 +02:00
khwezi 0c4c848568 Merge pull request 'Endpoints' (#6) from endpoints into main
Reviewed-on: #6
2026-08-30 16:11:53 +02:00
khwezi fb05150ebf Merge pull request 'Endpoints' (#5) from endpoints into main
Reviewed-on: #5
2026-08-30 15:47:32 +02:00
khwezi 2f9c20020b Merge pull request 'Reogranised API and Security assets' (#4) from endpoints into main
Reviewed-on: #4
2026-08-30 15:09:32 +02:00
khwezi a771957db3 Merge pull request 'Endpoints' (#3) from endpoints into main
Reviewed-on: #3
2026-08-30 14:54:58 +02:00
khwezi 46267a2a34 Merge pull request 'Endpoints' (#2) from endpoints into main
Reviewed-on: #2
2026-08-30 14:26:08 +02:00
khwezi a7f360cc2a Merge pull request 'Security' (#1) from security into main
Reviewed-on: #1
2026-08-21 11:47:36 +02:00
hermes 67c08b40ca Refactored gitignore 2026-08-21 11:45:33 +02:00
179 changed files with 31267 additions and 1597 deletions
@@ -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/invites/{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();
}
}
@@ -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,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);
}
}
@@ -15,11 +15,13 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint
public void Map(IEndpointRouteBuilder builder)
{
builder.MapPut("api/governance/organisations", async (UpdateOrganisationRequest request,
IGovernanceService service, CancellationToken cancellationToken = default) =>
await service.UpdateOrganisationAsync(request, cancellationToken) switch
IGovernanceService service, HttpContext context, CancellationToken cancellationToken = default) =>
await service.UpdateOrganisationAsync(request, context.GetUserId(), cancellationToken) switch
{
{ 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<ValidationError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Update an existing organization profile")
@@ -27,10 +29,10 @@ public sealed class UpdateOrganisationEndpoint : IEndpoint
.WithName(typeof(UpdateOrganisationEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status401Unauthorized)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status401Unauthorized)
.WithTags(EndpointTags.Governance);
}
}
@@ -1,36 +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.Tools;
namespace PostFundManagement.Api.Endpoints.Governance;
[ApiVersionTarget(1)]
public sealed class EncodeIdEndpoint : IEndpoint
public sealed class UpdateTenantEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder builder)
{
builder.MapGet("api/tools/hashid/{id64:long}", async (long id64, IToolService service,
CancellationToken cancellationToken) =>
await service.EncodeLong(id64, cancellationToken) switch
builder.MapPut("api/governance/tenants", async (UpdateTenantRequest request, HttpContext context,
IGovernanceService service, CancellationToken cancellationToken = default) =>
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<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
})
.WithDescription("Hashes an int64 identifier")
.WithDescription("Updates a specific tenant")
.RequireAuthorization(Constants.MainScope)
.WithName(typeof(EncodeIdEndpoint).ToEndpointName())
.WithName(typeof(UpdateTenantEndpoint).ToEndpointName())
.MapToApiVersion(new ApiVersion(1))
.Produces<string>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Tools)
.CacheOutput();
.WithTags(EndpointTags.Governance);
}
}
@@ -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/beneficiaries/{projectId}", 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/changerequests/{projectId}", 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/issues/{projectId}", 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/sitevisits/{projectId}", 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 PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Filters;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Health;
using PostFundManagement.Domain.Mediator;
using PostFundManagement.Infrastructure.Database.Services;
using static PostFundManagement.Domain.Extensions.Constants;
var builder = WebApplication.CreateBuilder(args);
@@ -27,6 +29,7 @@ builder.Services.AddApiVersioning(options =>
});
builder.Services.AddScoped<ActiveUserFilter>();
builder.Services.AddTransient<ITenantContextService, TenantContextService>();
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
builder.Services.AddApiServices(builder.Configuration);
+5 -2
View File
@@ -20,8 +20,11 @@
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Default": "Warning",
"Quartz": "Error",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database": "Error",
"Microsoft.EntityFrameworkCore.Update": "Error"
}
},
"EmailProcessorCron": "0 */5 * * * ?",
@@ -12,8 +12,8 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
public string GenerateCdnUrl(string fileKey) => $"{CdnBaseUrl}/{fileKey}";
public virtual async ValueTask<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
public virtual async Task<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
if (string.IsNullOrWhiteSpace(BucketName))
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}";
}
public virtual async ValueTask<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
public virtual async Task<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail<string>(ValidationError.Create("Bucket name is not configured."));
@@ -92,8 +92,8 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
: Result.Ok(fileKey);
}));
public virtual async ValueTask<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public virtual async Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
if (string.IsNullOrWhiteSpace(BucketName))
return Result.Fail(NotFoundError.Create("Bucket name is not configured."));
@@ -1,16 +1,36 @@
using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
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.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
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,41 @@
using PostFundManagement.Application.Services.Execution;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
public interface IExecutionService
{
ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default);
}
@@ -1,32 +1,81 @@
using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
public interface IGovernanceService
{
ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default);
[Log]
Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default);
ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Portfolio>> GetPortfolioAsync(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,55 @@
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
public interface IGrantsService
{
ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default);
ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default);
[Log]
[Audit]
Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default);
ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, 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
{
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.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
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]
Task<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default);
ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default);
[Log]
Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default);
ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default);
[Log]
Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default);
ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, 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
{
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.Domain;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Models;
namespace PostFundManagement.Application.Abstractions.Services;
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)
{
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
var connectionString = configuration.GetConnectionString(SchedulerDatabaseConfigName);
services.Configure<QuartzOptions>(options =>
{
@@ -44,7 +44,7 @@ public static class Quartz
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 =>
{
@@ -1,5 +1,6 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Interceptors;
using PostFundManagement.Application.Services.Communications;
using PostFundManagement.Application.Services.Evidence;
using PostFundManagement.Application.Services.Execution;
@@ -7,7 +8,6 @@ using PostFundManagement.Application.Services.Governance;
using PostFundManagement.Application.Services.Grants;
using PostFundManagement.Application.Services.Projects;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Application.Services.Tools;
using PostFundManagement.Application.Services.Users;
using PostFundManagement.Domain.Abstractions;
@@ -15,34 +15,71 @@ namespace PostFundManagement.Application.Extensions;
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)
{
services.AddScoped<ICommunicationsService, CommunicationsService>();
services.Decorate<ICommunicationsService, CommunicationsCacheService>();
services.AddScoped<AuditInterceptor>();
services.AddScoped<LoggingInterceptor>();
services.AddSingleton<ProxyGenerator>();
services.AddScoped<IEvidenceService, EvidenceService>();
services.Decorate<IEvidenceService, EvidenceCacheService>();
services.AddLoggedAuditedCachedServiceScoped<ICommunicationsService, CommunicationsService, CommunicationsCacheService>();
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.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.AddLoggedCachedServiceScoped<IIdentityService, IdentityService, IdentityCacheService>();
services.AddStackExchangeRedisCache(options =>
{
@@ -25,8 +25,12 @@ public sealed class ActiveUserFilter(IIdentityService identityService) : IEndpoi
var path = context.HttpContext.Request.Path.Value;
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[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 -->
<ItemGroup>
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.12" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
@@ -36,6 +37,7 @@
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Humanizer" />
<Using Include="Castle.DynamicProxy" />
<Using Include="Microsoft.FeatureManagement" />
<Using Include="System.ComponentModel.DataAnnotations" />
<Using Include="Microsoft.Extensions.Caching.Distributed" />
@@ -11,8 +11,11 @@ namespace PostFundManagement.Application.Services.Communications;
public sealed class CommunicationsCacheService(CommunicationsService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, ICommunicationsService
{
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
public async Task<Result> CreateNotificationAsync(CreateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
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))
return await service.GetApplicantInviteAsync(applicantUserId, cancellationToken);
@@ -33,8 +36,8 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
return Result.Ok(cached!.FromJson<Invite>());
}));
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
public async Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetApplicantInvitesAsync(applicantUserId, cancellationToken);
@@ -57,8 +60,8 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
return Result.Ok(cached.FromJson<Invite[]>());
}));
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
public async Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetInvitesAsync(pagination, cancellationToken);
@@ -81,6 +84,57 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID
return Result.Ok(cached.FromJson<Invite[]>());
}));
public async ValueTask<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await service.SendInviteAsync(request, userId, cancellationToken);
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);
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.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
@@ -7,10 +9,11 @@ using PostFundManagement.Infrastructure.Database;
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) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
public async Task<Result<Invite>> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Invite>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -22,28 +25,24 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
ContractId = request.ContractId,
Recipient = request.Recipient,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
CreatedAt = DateTime.UtcNow,
TenantId = tenantContext.TenantId
};
context.Invites.Add(invite);
context.Notifications.Add(new Domain.Entities.Notification
{
OrganisationId = request.OrganisationId,
Recipient = request.Recipient,
Platform = request.Platform,
Status = NotificationStatus.Pending,
Subject = "Invitation to Onboard",
Message = request.Message,
CreatedAt = DateTime.UtcNow
});
var notificationRequest = new CreateNotificationRequest(request.InvitedOrganisationId, request.Recipient,
request.Platform, "Invitation to Onboard", string.Empty, request.Message);
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());
}));
public async ValueTask<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
public async Task<Result<Invite[]>> GetInvitesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Invite>>>)(async () =>
public async Task<Result<Invite>> GetApplicantInviteAsync(Guid applicantUserId, CancellationToken cancellationToken) =>
await Result.Try((Func<Task<Result<Invite>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -67,8 +66,8 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
.ToResult(invite!.Map());
}));
public async ValueTask<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Invite[]>>>)(async () =>
public async Task<Result<Invite[]>> GetApplicantInvitesAsync(Guid applicantUserId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Invite[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -79,4 +78,79 @@ public sealed class CommunicationsService(IDbContextFactory<ApplicationDbContext
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;
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,
[Required] long InvitedOrganisationId,
[Required] long AwardId,
@@ -11,17 +11,17 @@ namespace PostFundManagement.Application.Services.Evidence;
public sealed class EvidenceCacheService(EvidenceService service, IDistributedCache cache,
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);
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);
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);
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence>>>)(async () =>
public async Task<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Evidence>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
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>());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
public async Task<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
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[]>());
}));
public async ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
public async Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetEvidenceUrlAsync(evidenceIdHash, cancellationToken);
@@ -92,8 +92,8 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached);
}));
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () =>
public async Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Indicator>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIndicatorAsync(indicatorId, cancellationToken);
@@ -116,8 +116,8 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached.FromJson<Indicator>());
}));
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
public async Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Indicator[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetIndicatorsAsync(pagination, cancellationToken);
@@ -140,8 +140,8 @@ public sealed class EvidenceCacheService(EvidenceService service, IDistributedCa
return Result.Ok(cached.FromJson<Indicator[]>());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
public async Task<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
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[]>());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
public async Task<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
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[]>());
}));
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);
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);
}
@@ -12,8 +12,8 @@ namespace PostFundManagement.Application.Services.Evidence;
public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService,
[FromKeyedServices(Constants.GeneralBucketName)] IS3Service s3Service) : IEvidenceService
{
public async ValueTask<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateEvidenceAsync(Guid userId, IFormCollection form, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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) ||
@@ -49,8 +49,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> RecordIndicatorActualAmountAsync(string indicatorIdHash, decimal amount, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(indicatorIdHash);
@@ -68,8 +68,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
public async Task<Result<Indicator[]>> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Indicator[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
public async Task<Result<Domain.Models.Evidence[]>> GetEvidenceFilesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateMilestoneAsync(CreateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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."));
}));
public async ValueTask<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateIndicatorAsync(CreateIndicatorRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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);
}));
public async ValueTask<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Indicator>>>)(async () =>
public async Task<Result<Indicator>> GetIndicatorAsync(string indicatorId, CancellationToken cancellationToken) =>
await Result.Try((Func<Task<Result<Indicator>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(indicatorId);
@@ -162,8 +162,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.ToResult(indicator!.Map());
}));
public async ValueTask<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence>>>)(async () =>
public async Task<Result<Domain.Models.Evidence>> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Evidence>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(evidenceId);
@@ -179,8 +179,8 @@ public sealed class EvidenceService(IDbContextFactory<ApplicationDbContext> cont
.ToResult(evidence!.Map());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
public async Task<Result<Domain.Models.Evidence[]>> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Evidence[]>>>)(async () =>
public async Task<Result<Domain.Models.Evidence[]>> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Evidence[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdateEvidenceAsync(UpdateEvidenceRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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);
}));
public async ValueTask<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
public async Task<Result<string>> GetEvidenceUrlAsync(string evidenceIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(evidenceIdHash);
@@ -11,17 +11,17 @@ namespace PostFundManagement.Application.Services.Execution;
public sealed class ExecutionCacheService(ExecutionService service, IDistributedCache cache,
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);
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);
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);
public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement>>>)(async () =>
public async Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Disbursement>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDisbursementAsync(disbursementIdHash, cancellationToken);
@@ -44,8 +44,8 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
return Result.Ok(cached.FromJson<Disbursement>());
}));
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement[]>>>)(async () =>
public async Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Disbursement[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetDisbursementsAsync(pagination, cancellationToken);
@@ -68,8 +68,8 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
return Result.Ok(cached.FromJson<Disbursement[]>());
}));
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk>>>)(async () =>
public async Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Risk>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRiskAsync(riskIdHash, cancellationToken);
@@ -92,8 +92,8 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
return Result.Ok(cached.FromJson<Risk>());
}));
public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
public async Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Risk[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetRisksAsync(pagination, cancellationToken);
@@ -116,9 +116,9 @@ public sealed class ExecutionCacheService(ExecutionService service, IDistributed
return Result.Ok(cached.FromJson<Risk[]>());
}));
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 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);
}
@@ -10,8 +10,8 @@ namespace PostFundManagement.Application.Services.Execution;
public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> contextFactory, HashService hashService) : IExecutionService
{
public async ValueTask<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
public async Task<Result<Risk[]>> GetRisksAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Risk[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -24,8 +24,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.Ok(risks.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Disbursement[]>>>)(async () =>
public async Task<Result<Disbursement[]>> GetDisbursementsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Disbursement[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -38,8 +38,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.Ok(disbursements.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateRiskAsync(CreateRiskRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -60,8 +60,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateDisbursementAsync(CreateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -81,8 +81,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> ApproveDisbursementAsync(string disbursementIdHash, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash);
@@ -101,8 +101,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Disbursement>>>)(async () =>
public async Task<Result<Disbursement>> GetDisbursementAsync(string disbursementIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<Task<Result<Disbursement>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(disbursementIdHash);
@@ -119,8 +119,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.ToResult(disbursement!.Map());
}));
public async ValueTask<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk>>>)(async () =>
public async Task<Result<Risk>> GetRiskAsync(string riskIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Risk>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(riskIdHash);
@@ -137,8 +137,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
.ToResult(risk!.Map());
}));
public async ValueTask<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdateRiskAsync(UpdateRiskRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -153,8 +153,8 @@ public sealed class ExecutionService(IDbContextFactory<ApplicationDbContext> con
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdateDisbursementAsync(UpdateDisbursementRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -11,14 +11,20 @@ namespace PostFundManagement.Application.Services.Governance;
public sealed class GovernanceCacheService(GovernanceService service, IDistributedCache cache,
IFeatureManager featureManager) : ServiceCacheBase, IGovernanceService
{
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 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);
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Dashboard>>>)(async () =>
public async Task<Result> CreateTenantAsync(CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) =>
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))
return await service.GetDashboardAsync(cancellationToken);
@@ -41,8 +47,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Dashboard>());
}));
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () =>
public async Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Instrument[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetInstrumentsAsync(programmeIdHash, cancellationToken);
@@ -65,8 +71,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Instrument[]>());
}));
public async ValueTask<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () =>
public async Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Organisation>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOrganisationAsync(organisationIdHash, cancellationToken);
@@ -89,8 +95,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Organisation>());
}));
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation[]>>>)(async () =>
public async Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Organisation[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetOrganisationsAsync(pagination, cancellationToken);
@@ -113,8 +119,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Organisation[]>());
}));
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () =>
public async Task<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Portfolio[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPorfoliosAsync(organisationIdHash, cancellationToken);
@@ -137,8 +143,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Portfolio[]>());
}));
public async ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio>>>)(async () =>
public async Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Portfolio>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPortfolioAsync(portfolioIdHash, cancellationToken);
@@ -161,8 +167,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Portfolio>());
}));
public async ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
public async Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetPortfolioProgrammesAsync(portfolioIdHash, cancellationToken);
@@ -185,8 +191,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Programme[]>());
}));
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
public async Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammesAsync(portfolioIdHash, cancellationToken);
@@ -209,9 +215,114 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut
return Result.Ok(cached.FromJson<Programme[]>());
}));
public async ValueTask<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) =>
await service.UpdateOrganisationAsync(request, cancellationToken);
public async Task<Result<Tenant>> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) =>
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) =>
await service.UpdatePortfolioAsync(request, userId, cancellationToken);
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<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);
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);
}
@@ -1,18 +1,20 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
using PostFundManagement.Infrastructure.Database;
using PostFundManagement.Application.Extensions;
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) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
public async Task<Result<Programme[]>> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio[]>>>)(async () =>
public async Task<Result<Portfolio[]>> GetPorfoliosAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Portfolio[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(organisationIdHash);
@@ -49,22 +51,23 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.Ok(portfolios.Select(p => p.Map()).ToArray());
}));
public async ValueTask<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Organisation[]>>>)(async () =>
public async Task<Result<Organisation[]>> GetOrganisationsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Organisation[]>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var organisations = await context.Organisations.AsNoTracking()
.OrderBy(o => o.Name)
var organisations = await context.TenantOrganisations.AsNoTracking()
.Include(i => i.Organisation)
.OrderBy(o => o.Organisation!.Name)
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.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) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () =>
public async Task<Result<Organisation>> GetOrganisationAsync(string organisationIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Organisation>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(organisationIdHash);
@@ -74,15 +77,16 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var organisation = await context.Organisations.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == decodeResult.Value, cancellationToken);
var tenantOrganisation = await context.TenantOrganisations.AsNoTracking()
.Include(i => i.OrganisationId)
.FirstOrDefaultAsync(o => o.OrganisationId == decodeResult.Value, cancellationToken);
return Result.FailIf(organisation is null, NotFoundError.New)
.ToResult(organisation!.Map());
return Result.FailIf(tenantOrganisation is null, NotFoundError.New)
.ToResult(tenantOrganisation!.Organisation!.Map());
}));
public async ValueTask<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Instrument[]>>>)(async () =>
public async Task<Result<Instrument[]>> GetInstrumentsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Instrument[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(programmeIdHash);
@@ -100,8 +104,8 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.Ok(instruments.Select(i => i.Map()).ToArray());
}));
public async ValueTask<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Dashboard>>>)(async () =>
public async Task<Result<Dashboard>> GetDashboardAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Dashboard>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -126,8 +130,8 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
disbursedAmount, onTrackPercentage, atRiskCount, beneficiariesReached));
}));
public async ValueTask<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateOrganisationAsync(CreateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -136,7 +140,7 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
if (await context.Organisations.AnyAsync(o => o.RegistrationNo == registrationNo, cancellationToken))
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(),
RegistrationNo = registrationNo,
@@ -147,11 +151,16 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
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) =>
await Result.Try((Func<ValueTask<Result<Organisation>>>)(async () =>
public async Task<Result<Organisation>> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Organisation>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -161,13 +170,21 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
.SetProperty(u => u.Name, request.Name.Humanize(LetterCasing.Title).Trim())
.SetProperty(u => u.RegistrationNo, request.RegistrationNo.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.IsFailed, UpdateError.Create("Failed to update organisation"));
}));
public async ValueTask<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -189,14 +206,15 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
OrganisationId = request.OrganisationId,
OwnedBy = request.OwnerId,
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);
}));
public async ValueTask<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -213,8 +231,8 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.OkIf(changedTotal > 0, UpdateError.Create("Nothing was saved, no change detected."));
}));
public async ValueTask<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
public async Task<Result<Programme[]>> GetPortfolioProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash);
@@ -232,8 +250,8 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.Ok(programmes.Select(p => p.Map()).ToArray());
}));
public async ValueTask<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Portfolio>>>)(async () =>
public async Task<Result<Portfolio>> GetPortfolioAsync(string portfolioIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Portfolio>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(portfolioIdHash);
@@ -249,4 +267,172 @@ public sealed class GovernanceService(IDbContextFactory<ApplicationDbContext> co
return Result.FailIf(portfolio is null, NotFoundError.New)
.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());
}));
}
@@ -3,6 +3,16 @@ using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Services.Governance;
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,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(TextLength)] string Description,
@@ -17,7 +27,8 @@ public sealed record UpdateOrganisationRequest([Required] long OrganisationId,
[Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string RegistrationNo,
[Required][EmailAddress][MaxLength(LabelLength)] string Email,
[Required] OrganisationType Type);
[Required] OrganisationType Type,
[Required] ActivityStatus Status);
public sealed record CreateOrganisationRequest([Required][MaxLength(LabelLength)] string Name,
[Required][MaxLength(LabelLength)] string RegistrationNo,
@@ -11,20 +11,20 @@ namespace PostFundManagement.Application.Services.Grants;
public sealed class GrantsCacheService(GrantsService service, IDistributedCache cache,
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);
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);
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);
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);
public async ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award>>>)(async () =>
public async Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Award>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetAwardAsync(awardIdHash, cancellationToken);
@@ -47,8 +47,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Award>());
}));
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award[]>>>)(async () =>
public async Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Award[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetAwardsAsync(pagination, cancellationToken);
@@ -71,8 +71,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Award[]>());
}));
public async ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget>>>)(async () =>
public async Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Budget>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBudgetAsync(budgetIdHash, cancellationToken);
@@ -95,8 +95,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Budget>());
}));
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget[]>>>)(async () =>
public async Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Budget[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetBudgetsAsync(pagination, cancellationToken);
@@ -119,8 +119,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Budget[]>());
}));
public async ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract>>>)(async () =>
public async Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Contract>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractAsync(contractIdHash, cancellationToken);
@@ -143,8 +143,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Contract>());
}));
public async ValueTask<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () =>
public async Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Contract[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractsAsync(pagination, cancellationToken);
@@ -167,8 +167,8 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached.FromJson<Contract[]>());
}));
public async ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
public async Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetContractUrlAsync(contractIdHash, cancellationToken);
@@ -191,9 +191,9 @@ public sealed class GrantsCacheService(GrantsService service, IDistributedCache
return Result.Ok(cached!);
}));
public async ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
public async Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
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);
}
@@ -10,10 +10,10 @@ using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Grants;
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) =>
await Result.Try((Func<ValueTask<Result<Contract[]>>>)(async () =>
public async Task<Result<Contract[]>> GetContractsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Contract[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Contract>>>)(async () =>
public async Task<Result<Contract>> GetContractAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Contract>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
@@ -44,8 +44,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.ToResult(contract!.Map());
}));
public async ValueTask<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget[]>>>)(async () =>
public async Task<Result<Budget[]>> GetBudgetsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Budget[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Budget>>>)(async () =>
public async Task<Result<Budget>> GetBudgetAsync(string budgetIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Budget>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(budgetIdHash);
@@ -76,8 +76,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.ToResult(budget!.Map());
}));
public async ValueTask<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award[]>>>)(async () =>
public async Task<Result<Award[]>> GetAwardsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Award[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Award>>>)(async () =>
public async Task<Result<Award>> GetAwardAsync(string awardIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Award>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(awardIdHash);
@@ -107,8 +107,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.ToResult(award!.Map());
}));
public async ValueTask<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> ExecuteContractAsync(ExecuteContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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);
}));
public async ValueTask<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateContractAsync(CreateContractRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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);
}));
public async ValueTask<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateBudgetAsync(CreateBudgetRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
if (request.Amount <= 0)
return Result.Fail(ValidationError.Create("Budget amount must be greater than zero."));
@@ -175,8 +175,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateAwardAsync(CreateAwardRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
if (request.Amount <= 0)
return Result.Fail(ValidationError.Create("Approved amount must be greater than zero."));
@@ -196,7 +196,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
Amount = request.Amount,
Status = ApprovalStatus.Pending,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
CreatedAt = DateTime.UtcNow,
TenantId = tenantContext.TenantId
};
context.Awards.Add(award);
@@ -204,8 +205,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
public async Task<Result<string>> GetContractUrlAsync(string contractIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
@@ -231,8 +232,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
.ToResult(secureDownloadUrlResult.Value);
}));
public async ValueTask<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UploadContractAsync(string contractIdHash, Guid userId, IFormFile file, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(contractIdHash);
@@ -261,8 +262,8 @@ public sealed class GrantsService(IDbContextFactory<ApplicationDbContext> contex
CreateError.Create("Failed to upload contract file"));
}));
public async ValueTask<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdateContractAsync(UpdateContractRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -11,17 +11,35 @@ namespace PostFundManagement.Application.Services.Projects;
public sealed class ProjectCacheService(ProjectService service, IDistributedCache cache,
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);
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);
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);
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity[]>>>)(async () =>
public async Task<Result> CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) =>
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))
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[]>());
}));
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity>>>)(async () =>
public async Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Activity>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
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>());
}));
public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone>>>)(async () =>
public async Task<Result<Beneficiary[]>> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
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))
return await service.GetMilestoneAsync(milestoneIdHash, cancellationToken);
@@ -92,8 +302,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Milestone>());
}));
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
public async Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Milestone[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetMilestonesAsync(pagination, cancellationToken);
@@ -116,8 +326,56 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Milestone[]>());
}));
public async ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme>>>)(async () =>
public async Task<Result<Outcome>> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) =>
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))
return await service.GetProgrammeAsync(programmeIdHash, cancellationToken);
@@ -140,8 +398,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached!.FromJson<Programme>());
}));
public async ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
public async Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Project[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammeProjectsAsync(programmeIdHash, cancellationToken);
@@ -164,8 +422,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Project[]>());
}));
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
public async Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProgrammesAsync(pagination, cancellationToken);
@@ -188,8 +446,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Programme[]>());
}));
public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project>>>)(async () =>
public async Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Project>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectAsync(projectIdHash, cancellationToken);
@@ -212,8 +470,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Project>());
}));
public async ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
public async Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Indicator[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectIndicatorsAsync(projectIdHash, cancellationToken);
@@ -236,8 +494,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Indicator[]>());
}));
public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
public async Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Milestone[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectMilestonesAsync(projectIdHash, cancellationToken);
@@ -260,8 +518,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Milestone[]>());
}));
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
public async Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Risk[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectRisksAsync(projectIdHash, cancellationToken);
@@ -284,8 +542,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
return Result.Ok(cached.FromJson<Risk[]>());
}));
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
public async Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Project[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetProjectsAsync(pagination, cancellationToken);
@@ -308,15 +566,81 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach
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);
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);
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);
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);
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.Services.Shared;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
@@ -8,10 +9,11 @@ using PostFundManagement.Infrastructure.Database;
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) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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."));
}));
public async ValueTask<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
public async Task<Result<Project[]>> GetProjectsAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Project[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project>>>)(async () =>
public async Task<Result<Project>> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Project>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(projectIdHash);
@@ -59,8 +61,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.ToResult(project!.Map());
}));
public async ValueTask<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity[]>>>)(async () =>
public async Task<Result<Domain.Models.Activity[]>> GetActivitiesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Activity[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateProjectAsync(CreateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -101,15 +103,16 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
ProjectType = request.ProjectType,
Status = ApprovalStatus.Pending,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow
CreatedAt = DateTime.UtcNow,
TenantId = tenantContext.TenantId
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0,
CreateError.Create("No changes detected, update cancelled."));
}));
public async ValueTask<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateActivityAsync(CreateActivityRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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."));
}));
public async ValueTask<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
public async Task<Result<Milestone[]>> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Milestone[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<Indicator[]>>>)(async () =>
public async Task<Result<Indicator[]>> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) =>
await Result.Try((Func<Task<Result<Indicator[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone>>>)(async () =>
public async Task<Result<Milestone>> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Milestone>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash);
@@ -193,8 +196,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.ToResult(milestone!.Map());
}));
public async ValueTask<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Milestone[]>>>)(async () =>
public async Task<Result<Milestone[]>> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Milestone[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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."));
}));
public async ValueTask<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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."));
}));
public async ValueTask<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Models.Activity>>>)(async () =>
public async Task<Result<Domain.Models.Activity>> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Activity>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(activityIdHash);
@@ -264,8 +267,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.ToResult(activity!.Map());
}));
public async ValueTask<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Risk[]>>>)(async () =>
public async Task<Result<Risk[]>> GetProjectRisksAsync(string projectIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Risk[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> CreateProgrammeAsync(CreateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -303,14 +306,15 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
Name = name,
CreatedBy = userId,
CreatedAt = DateTime.UtcNow,
Status = ApprovalStatus.Pending
Status = ApprovalStatus.Pending,
TenantId = tenantContext.TenantId
});
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdateProgrammeAsync(UpdateProgrammeRequest request, Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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."));
}));
public async ValueTask<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme>>>)(async () =>
public async Task<Result<Programme>> GetProgrammeAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Programme>>>)(async () =>
{
var decodeResult = hashService.DecodeLongIdHash(programmeIdHash);
@@ -344,8 +348,8 @@ public sealed class ProjectService(IDbContextFactory<ApplicationDbContext> conte
.ToResult(programme!.Map());
}));
public async ValueTask<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Programme[]>>>)(async () =>
public async Task<Result<Programme[]>> GetProgrammesAsync(Pagination pagination, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Programme[]>>>)(async () =>
{
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());
}));
public async ValueTask<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Project[]>>>)(async () =>
public async Task<Result<Project[]>> GetProgrammeProjectsAsync(string programmeIdHash, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Project[]>>>)(async () =>
{
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());
}));
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;
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,
[Required][MaxLength(LabelLength)] string Name,
[Required] Guid OwnerId,
@@ -0,0 +1,27 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Services.Shared;
public sealed class AuditService(IDbContextFactory<ApplicationDbContext> contextFactory, ITenantContextService tenantContextService) : IService
{
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 async ValueTask<Result> DeleteAsync(string key) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> DeleteAsync(string key) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await storage.DeleteAsync(key);
return Result.Ok();
}));
public async ValueTask<Result> SaveAsync(string key, string value) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> SaveAsync(string key, string value) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await storage.SetAsync(key, value);
return Result.Ok();
}));
public async ValueTask<Result> SaveAsync<TValue>(string key, TValue value) where TValue : class =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> SaveAsync<TValue>(string key, TValue value) where TValue : class =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await storage.SetAsync(key, value);
return Result.Ok();
}));
public async ValueTask<Result<string>> GetAsync(string key) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
public async Task<Result<string>> GetAsync(string key) =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
var retrieval = await storage.GetAsync<string>(key);
@@ -38,8 +38,8 @@ public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
.ToResult(retrieval.Value!);
}));
public async ValueTask<Result<TValue>> GetAsync<TValue>(string key) where TValue : class =>
await Result.Try((Func<ValueTask<Result<TValue>>>)(async () =>
public async Task<Result<TValue>> GetAsync<TValue>(string key) where TValue : class =>
await Result.Try((Func<Task<Result<TValue>>>)(async () =>
{
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 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");
@@ -121,7 +121,7 @@ public sealed class EmailService(IOptions<SmtpSettings> options) : IDisposable
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");
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");
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) =>
await Result.Try((Func<ValueTask<Result<string>>>)(async () =>
await Result.Try((Func<Task<Result<string>>>)(async () =>
{
string decoded = hasher.DecodeHex(hex);
@@ -5,6 +5,6 @@ namespace PostFundManagement.Application.Services.Shared;
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);
}
@@ -7,11 +7,12 @@ namespace PostFundManagement.Application.Services.Shared;
public sealed class IdentityService(IDbContextFactory<ApplicationDbContext> contextFactory) : IIdentityService
{
public async ValueTask<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UserIsAuthorisedAsync(Guid userId, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
// TODO: Ensure TenantUser status is checked
var userExistsAndActive = await context.Users
.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;
public async ValueTask<Result<Domain.Security.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
public async Task<Result<Domain.Security.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
{
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!);
}
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);
@@ -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,
IFeatureManager featureManager) : ServiceCacheBase, IUserService
{
public async ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<User>>>)(async () =>
public async Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<Task<Result<User>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
return await service.GetUserAsync(userId, cancellationToken);
@@ -35,8 +35,8 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach
return Result.Ok(cached.FromJson<User>());
}));
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<User[]>>>)(async () =>
public async Task<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<User[]>>>)(async () =>
{
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
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[]>());
}));
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);
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);
}
@@ -1,5 +1,6 @@
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Domain;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Models;
@@ -7,16 +8,19 @@ using PostFundManagement.Infrastructure.Database;
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) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> SyncUserAsync(Guid userId, string email, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
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
{
Id = userId,
@@ -24,35 +28,45 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
Status = ActivityStatus.Active,
LastLoginAt = DateTime.UtcNow
});
context.TenantUsers.Add(new Domain.Entities.TenantUser
{
CreatedAt = DateTime.UtcNow,
CreatedBy = userId,
Status = ActivityStatus.Active,
TenantId = tenantContext.TenantId,
UserId = userId
});
}
else
{
user!.Email = email;
user.LastLoginAt = DateTime.UtcNow;
tenantUser.User!.Email = email;
tenantUser.User.LastLoginAt = DateTime.UtcNow;
context.Users.Update(user);
context.Users.Update(tenantUser.User);
}
return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New);
}));
public async ValueTask<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<ValueTask<Result<User[]>>>)(async () =>
public async Task<Result<User[]>> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<User[]>>>)(async () =>
{
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)
query = query.Where(u => u.Status == request.Status.Value);
if (!string.IsNullOrWhiteSpace(request.Email))
query = query.Where(u => u.Email != null && u.Email.Contains(request.Email));
query = query.Where(u => u.User!.Email != null && u.User!.Email.Contains(request.Email));
if (!string.IsNullOrWhiteSpace(request.SortBy))
query = request.SortBy.ToLowerInvariant() switch
{
"email" => request.SortDescending ? query.OrderByDescending(u => u.Email) : query.OrderBy(u => u.Email),
"lastloginat" => request.SortDescending ? query.OrderByDescending(u => u.LastLoginAt) : query.OrderBy(u => u.LastLoginAt),
"email" => request.SortDescending ? query.OrderByDescending(u => u.User!.Email) : query.OrderBy(u => u.User!.Email),
"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),
_ => 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)
.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) =>
await Result.Try((Func<ValueTask<Result>>)(async () =>
public async Task<Result> UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var changedTotal = await context.Users
.Where(f => f.Id == userId)
var changedTotal = await context.TenantUsers
.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);
return Result.OkIf(changedTotal > 0, UpdateError.New);
}));
public async ValueTask<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<ValueTask<Result<User>>>)(async () =>
public async Task<Result<User>> GetUserAsync(Guid userId, CancellationToken cancellationToken) =>
await Result.Try((Func<Task<Result<User>>>)(async () =>
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var user = await context.Users.AsNoTracking()
.FirstOrDefaultAsync(f => f.Id == userId, cancellationToken);
var tenantUser = await context.TenantUsers.AsNoTracking()
.Include(i => i.User)
.FirstOrDefaultAsync(f => f.UserId == userId, cancellationToken);
return Result.FailIf(user is null, NotFoundError.New)
.ToResult(user!.Map());
return Result.FailIf(tenantUser == null, NotFoundError.New)
.ToResult(tenantUser!.User?.Map()!);
}));
}
@@ -3,7 +3,7 @@ namespace PostFundManagement.Domain.Abstractions;
public interface IS3Service
{
string GenerateCdnUrl(string fileKey);
ValueTask<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default);
ValueTask<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
ValueTask<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default);
Task<Result<string>> GeneratePresignedUrlAsync(string fileKey, TimeSpan duration, string? contentType = null, CancellationToken cancellationToken = default);
Task<Result<string>> UploadFileAsync(string fileName, string subPath, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,9 @@
namespace PostFundManagement.Domain.Abstractions;
public interface ITenantContextService
{
string? TenantName { get; }
long TenantId { get; }
bool IsAdmin { get; }
bool HasTenantFilter { get; }
}
@@ -0,0 +1,6 @@
namespace PostFundManagement.Domain.Abstractions;
public interface ITenantEntity
{
public long TenantId { get; set; }
}
@@ -0,0 +1,4 @@
namespace PostFundManagement.Domain.Attributes;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class AuditAttribute : Attribute;
@@ -0,0 +1,4 @@
namespace PostFundManagement.Domain.Attributes;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class LogAttribute : Attribute;
@@ -1,57 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Activity : IEntityTypeConfiguration<Domain.Entities.Activity>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Activity> builder)
{
builder.ToTable(nameof(Domain.Entities.Activity).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.MilestoneId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false);
builder.Property(f => f.ApprovedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.PerformedAt).IsRequired(false);
builder.Property(f => f.Category).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Status).IsRequired().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending);
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength);
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Milestone)
.WithMany()
.HasForeignKey(fk => fk.MilestoneId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Updator)
.WithMany()
.HasForeignKey(fk => fk.UpdatedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Approver)
.WithMany()
.HasForeignKey(fk => fk.ApprovedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,25 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class AuditLog : IEntityTypeConfiguration<Domain.Entities.AuditLog>
{
public void Configure(EntityTypeBuilder<Domain.Entities.AuditLog> builder)
{
builder.ToTable(nameof(Domain.Entities.AuditLog).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.EntityName).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.EntityId).IsRequired();
builder.Property(f => f.Action).IsRequired().HasMaxLength(ShortLabelLength);
builder.Property(f => f.Changes).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.PerformedBy).IsRequired();
builder.Property(f => f.Timestamp).IsRequired().HasDefaultValueSql("now()");
builder.HasOne(f => f.Performer)
.WithMany()
.HasForeignKey(fk => fk.PerformedBy)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -1,58 +0,0 @@
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Award : IEntityTypeConfiguration<Domain.Entities.Award>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Award> builder)
{
builder.ToTable(nameof(Domain.Entities.Award).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.OrganisationId).IsRequired();
builder.Property(f => f.ProgrammeId).IsRequired();
builder.Property(f => f.ProjectId).IsRequired(false);
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false);
builder.Property(f => f.ApprovedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending);
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
builder.HasOne(f => f.Organisation)
.WithMany()
.HasForeignKey(fk => fk.OrganisationId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Programme)
.WithMany(f => f.Awards)
.HasForeignKey(fk => fk.ProgrammeId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Project)
.WithMany(f => f.Awards)
.HasForeignKey(fk => fk.ProjectId)
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Updator)
.WithMany()
.HasForeignKey(fk => fk.UpdatedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Approver)
.WithMany()
.HasForeignKey(fk => fk.ApprovedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,40 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Beneficiary : IEntityTypeConfiguration<Domain.Entities.Beneficiary>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Beneficiary> builder)
{
builder.ToTable(nameof(Domain.Entities.Beneficiary).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UserId).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Category).IsRequired().HasMaxLength(ShortLabelLength);
builder.Property(f => f.TargetCount).IsRequired();
builder.Property(f => f.ReachedCount).IsRequired();
builder.Property(f => f.Location).IsRequired();
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.User)
.WithMany()
.HasForeignKey(fk => fk.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,38 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Budget : IEntityTypeConfiguration<Domain.Entities.Budget>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Budget> builder)
{
builder.ToTable(nameof(Domain.Entities.Budget).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.ApprovedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Approver)
.WithMany()
.HasForeignKey(fk => fk.ApprovedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,42 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class ChangeRequest : IEntityTypeConfiguration<Domain.Entities.ChangeRequest>
{
public void Configure(EntityTypeBuilder<Domain.Entities.ChangeRequest> builder)
{
builder.ToTable(nameof(Domain.Entities.ChangeRequest).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.ApprovedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Justification).IsRequired().HasMaxLength(TextLength);
builder.Property(f => f.RevisedBudget).IsRequired(false).HasPrecision(18, 2);
builder.Property(f => f.RevisedEndedAt).IsRequired(false);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending);
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Approver)
.WithMany()
.HasForeignKey(fk => fk.ApprovedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,40 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class ComplianceItem : IEntityTypeConfiguration<Domain.Entities.ComplianceItem>
{
public void Configure(EntityTypeBuilder<Domain.Entities.ComplianceItem> builder)
{
builder.ToTable(nameof(Domain.Entities.ComplianceItem).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.VerifiedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Requirements).IsRequired().HasMaxLength(MediumTextLength);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending);
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Verifier)
.WithMany()
.HasForeignKey(fk => fk.VerifiedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,35 +0,0 @@
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Contract : IEntityTypeConfiguration<Domain.Entities.Contract>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Contract> builder)
{
builder.ToTable(nameof(Domain.Entities.Contract).Pluralize());
builder.HasKey(pk => pk.Id);
builder.HasIndex(f => f.AwardId).IsUnique();
builder.Property(f => f.AwardId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.ExternalSignatureId).IsRequired(false);
builder.Property(f => f.SignedDocumentUrl).IsRequired(false);
builder.Property(f => f.EffectiveAt).IsRequired(false);
builder.Property(f => f.ExpiresAt).IsRequired(false);
builder.Property(f => f.TotalValue).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ContractStatus.Draft);
builder.HasOne(f => f.Award)
.WithOne(f => f.Contract)
.HasForeignKey<Domain.Entities.Contract>(fk => fk.AwardId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,44 +0,0 @@
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Disbursement : IEntityTypeConfiguration<Domain.Entities.Disbursement>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Disbursement> builder)
{
builder.ToTable(nameof(Domain.Entities.Disbursement).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.MilestoneId).IsRequired(false);
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending);
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Milestone)
.WithMany()
.HasForeignKey(fk => fk.MilestoneId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Updator)
.WithMany()
.HasForeignKey(fk => fk.UpdatedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,61 +0,0 @@
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Evidence : IEntityTypeConfiguration<Domain.Entities.Evidence>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Evidence> builder)
{
builder.ToTable(nameof(Domain.Entities.Evidence).Pluralize());
builder.HasKey(f => f.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.MilestoneId).IsRequired();
builder.Property(f => f.IndicatorId).IsRequired();
builder.Property(f => f.ActivityId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.Version).IsRequired().HasDefaultValue(1);
builder.Property(f => f.DocumentUrl).IsRequired();
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending);
builder.Property(f => f.Type).IsRequired().HasConversion<int>().HasDefaultValueSql("9")
.HasSentinel(EvidenceType.ProcurementDocument);
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Milestone)
.WithMany()
.HasForeignKey(fk => fk.MilestoneId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Indicator)
.WithMany()
.HasForeignKey(fk => fk.IndicatorId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Activity)
.WithMany(f => f.Evidences)
.HasForeignKey(fk => fk.ActivityId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Updator)
.WithMany()
.HasForeignKey(fk => fk.UpdatedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,33 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Indicator : IEntityTypeConfiguration<Domain.Entities.Indicator>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Indicator> builder)
{
builder.ToTable(nameof(Domain.Entities.Indicator).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.UnitOfMeasure).IsRequired().HasConversion<int>().HasDefaultValueSql("2");
builder.Property(f => f.BaselineAmount).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.TargetAmount).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.ActualAmount).IsRequired().HasPrecision(18, 2);
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,33 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Instrument : IEntityTypeConfiguration<Domain.Entities.Instrument>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Instrument> builder)
{
builder.ToTable(nameof(Domain.Entities.Instrument).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProgrammeId).IsRequired();
builder.Property(f => f.Code).IsRequired().HasMaxLength(ShortLabelLength);
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Description).IsRequired(false).HasMaxLength(TextLength);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("2")
.HasSentinel(ActivityStatus.Inactive);
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.HasOne(f => f.Programme)
.WithMany(f => f.Instruments)
.HasForeignKey(fk => fk.ProgrammeId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,51 +0,0 @@
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Invite : IEntityTypeConfiguration<Domain.Entities.Invite>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Invite> builder)
{
builder.ToTable(nameof(Domain.Entities.Invite).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.OrganisationId).IsRequired();
builder.Property(f => f.InvitedOrganisationId).IsRequired();
builder.Property(f => f.AwardId).IsRequired();
builder.Property(f => f.ContractId).IsRequired();
builder.Property(f => f.Recipient).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.HasOne(f => f.Organisation)
.WithMany()
.HasForeignKey(fk => fk.OrganisationId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.InvitedOrganisation)
.WithMany()
.HasForeignKey(fk => fk.InvitedOrganisationId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Award)
.WithMany()
.HasForeignKey(fk => fk.AwardId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Contract)
.WithMany()
.HasForeignKey(fk => fk.ContractId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.RecipientUser)
.WithMany()
.HasForeignKey(fk => fk.Recipient)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -1,43 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Issue : IEntityTypeConfiguration<Domain.Entities.Issue>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Issue> builder)
{
builder.ToTable(nameof(Domain.Entities.Issue).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.ResolvedAt).IsRequired(false);
builder.Property(f => f.AssignedTo).IsRequired(false);
builder.Property(f => f.Title).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength);
builder.Property(f => f.Priority).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(Priority.Low);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ActivityStatus.Active);
builder.Property(f => f.Resolution).IsRequired();
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Assignee)
.WithMany()
.HasForeignKey(fk => fk.AssignedTo)
.IsRequired()
.OnDelete(DeleteBehavior.SetNull);
}
}
@@ -1,42 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Milestone : IEntityTypeConfiguration<Domain.Entities.Milestone>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Milestone> builder)
{
builder.ToTable(nameof(Domain.Entities.Milestone).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProjectId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.DueAt).IsRequired();
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending);
builder.Property(f => f.Priority).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(Priority.Low);
builder.HasOne(f => f.Project)
.WithMany()
.HasForeignKey(fk => fk.ProjectId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Updator)
.WithMany()
.HasForeignKey(fk => fk.UpdatedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,38 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Notification : IEntityTypeConfiguration<Domain.Entities.Notification>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Notification> builder)
{
builder.ToTable(nameof(Domain.Entities.Notification).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.SentAt).IsRequired(false);
builder.Property(f => f.Recipient).IsRequired();
builder.Property(f => f.OrganisationId).IsRequired();
builder.Property(f => f.Platform).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(NotificationPlatform.Email);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(NotificationStatus.Pending);
builder.Property(f => f.Subject).IsRequired(false).HasMaxLength(LabelLength);
builder.Property(f => f.Message).IsRequired().HasMaxLength(LargeTextLength);
builder.Property(f => f.Destination).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Error).IsRequired(false);
builder.HasOne(f => f.Organisation)
.WithMany()
.HasForeignKey(fk => fk.OrganisationId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.RecipientUser)
.WithMany()
.HasForeignKey(fk => fk.Recipient)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -1,28 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Organisation : IEntityTypeConfiguration<Domain.Entities.Organisation>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Organisation> builder)
{
builder.ToTable(nameof(Domain.Entities.Organisation).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.RegistrationNo).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Email).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Type).IsRequired().HasConversion<int>().HasDefaultValueSql("5")
.HasSentinel(OrganisationType.PrivateLimitedCompany);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ActivityStatus.Active);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,30 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Outcome : IEntityTypeConfiguration<Domain.Entities.Outcome>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Outcome> builder)
{
builder.ToTable(nameof(Domain.Entities.Outcome).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.BeneficiaryId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength);
builder.HasOne(f => f.Beneficiary)
.WithMany()
.HasForeignKey(fk => fk.BeneficiaryId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,37 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Portfolio : IEntityTypeConfiguration<Domain.Entities.Portfolio>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Portfolio> builder)
{
builder.ToTable(nameof(Domain.Entities.Portfolio).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.OrganisationId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.OwnedBy).IsRequired();
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Description).IsRequired(false).HasMaxLength(TextLength);
builder.HasOne(f => f.Organisation)
.WithMany()
.HasForeignKey(fk => fk.OrganisationId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Owner)
.WithMany()
.HasForeignKey(fk => fk.OwnedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,40 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Programme : IEntityTypeConfiguration<Domain.Entities.Programme>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Programme> builder)
{
builder.ToTable(nameof(Domain.Entities.Programme).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.PortfolioId).IsRequired();
builder.Property(f => f.OwnedBy).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending);
builder.HasOne(f => f.Portfolio)
.WithMany(f => f.Programmes)
.HasForeignKey(fk => fk.PortfolioId)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Owner)
.WithMany()
.IsRequired()
.HasForeignKey(fk => fk.OwnedBy)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,46 +0,0 @@
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Configuration.Entities;
public sealed class Project : IEntityTypeConfiguration<Domain.Entities.Project>
{
public void Configure(EntityTypeBuilder<Domain.Entities.Project> builder)
{
builder.ToTable(nameof(Domain.Entities.Project).Pluralize());
builder.HasKey(pk => pk.Id);
builder.Property(f => f.ProgrammeId).IsRequired();
builder.Property(f => f.CreatedBy).IsRequired();
builder.Property(f => f.UpdatedBy).IsRequired(false);
builder.Property(f => f.CreatedAt).IsRequired().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.StartedAt).IsRequired(false);
builder.Property(f => f.EndedAt).IsRequired(false);
builder.Property(f => f.Name).IsRequired().HasMaxLength(LabelLength);
builder.Property(f => f.Description).IsRequired().HasMaxLength(TextLength);
builder.Property(f => f.Sector).IsRequired().HasConversion<int>().HasDefaultValueSql("11")
.HasSentinel(IndustrySector.InformationCommunicationTechnology);
builder.Property(f => f.ProjectType).IsRequired().HasConversion<int>().HasDefaultValueSql("10")
.HasSentinel(ProjectType.Innovation);
builder.Property(f => f.Status).IsRequired().HasConversion<int>().HasDefaultValueSql("1")
.HasSentinel(ApprovalStatus.Pending);
builder.HasOne(f => f.Programme)
.WithMany(f => f.Projects)
.HasForeignKey(fk => fk.ProgrammeId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Creator)
.WithMany()
.HasForeignKey(fk => fk.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(f => f.Updator)
.WithMany()
.HasForeignKey(fk => fk.UpdatedBy)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
}
}

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