diff --git a/PostFundManagement.Api/Endpoints/Communications/GetNotificationsEndpoint.cs b/PostFundManagement.Api/Endpoints/Communications/GetNotificationsEndpoint.cs new file mode 100644 index 0000000..5454dba --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Communications/GetNotificationsEndpoint.cs @@ -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() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Communications) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/CreateTenantEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/CreateTenantEndpoint.cs new file mode 100644 index 0000000..572928e --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/CreateTenantEndpoint.cs @@ -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() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/GetTenantEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetTenantEndpoint.cs new file mode 100644 index 0000000..5ac8bdb --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/GetTenantEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs new file mode 100644 index 0000000..2286976 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/GetTenantsEndpoint.cs @@ -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()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Api/Endpoints/Governance/UpdateOrganisationEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/UpdateOrganisationEndpoint.cs index 6fa659a..085beac 100644 --- a/PostFundManagement.Api/Endpoints/Governance/UpdateOrganisationEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Governance/UpdateOrganisationEndpoint.cs @@ -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() => Results.Forbid(), var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => 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(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status500InternalServerError) - .Produces(StatusCodes.Status401Unauthorized) .WithTags(EndpointTags.Governance); } } diff --git a/PostFundManagement.Api/Endpoints/Governance/UpdateTenantEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/UpdateTenantEndpoint.cs new file mode 100644 index 0000000..949522f --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Governance/UpdateTenantEndpoint.cs @@ -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 UpdateTenantEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + 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 } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Updates a specific tenant") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(UpdateTenantEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Governance); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateBeneficiaryEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateBeneficiaryEndpoint.cs new file mode 100644 index 0000000..0787805 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateBeneficiaryEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateChangeRequestEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateChangeRequestEndpoint.cs new file mode 100644 index 0000000..f78ca4c --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateChangeRequestEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateComplianceItemEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateComplianceItemEndpoint.cs new file mode 100644 index 0000000..85397c2 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateComplianceItemEndpoint.cs @@ -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() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateIssueEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateIssueEndpoint.cs new file mode 100644 index 0000000..2eb631b --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateIssueEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateOutcomeEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateOutcomeEndpoint.cs new file mode 100644 index 0000000..e410127 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateOutcomeEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} + \ No newline at end of file diff --git a/PostFundManagement.Api/Endpoints/Projects/CreateSiteVisitEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/CreateSiteVisitEndpoint.cs new file mode 100644 index 0000000..5de8bde --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/CreateSiteVisitEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs new file mode 100644 index 0000000..1812765 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiariesEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetBeneficiaryEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiaryEndpoint.cs new file mode 100644 index 0000000..0af8e92 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetBeneficiaryEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestEndpoint.cs new file mode 100644 index 0000000..a71792d --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestsEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestsEndpoint.cs new file mode 100644 index 0000000..4d2ba3e --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetChangeRequestsEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemEndpoint.cs new file mode 100644 index 0000000..0a0ed2e --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemsEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemsEndpoint.cs new file mode 100644 index 0000000..3579d22 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetComplianceItemsEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetIssueEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetIssueEndpoint.cs new file mode 100644 index 0000000..983b642 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetIssueEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetIssuesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetIssuesEndpoint.cs new file mode 100644 index 0000000..1966815 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetIssuesEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetOutcomeEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetOutcomeEndpoint.cs new file mode 100644 index 0000000..ac81816 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetOutcomeEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetOutcomesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetOutcomesEndpoint.cs new file mode 100644 index 0000000..e2f4e9f --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetOutcomesEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitEndpoint.cs new file mode 100644 index 0000000..bbe08f7 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitsEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitsEndpoint.cs new file mode 100644 index 0000000..cb20d17 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetSiteVisitsEndpoint.cs @@ -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() => Results.Forbid(), + var r when r.HasError() => 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(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateBeneficiaryEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateBeneficiaryEndpoint.cs new file mode 100644 index 0000000..255b45b --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateBeneficiaryEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateChangeRequestEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateChangeRequestEndpoint.cs new file mode 100644 index 0000000..3368070 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateChangeRequestEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateComplianceItemEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateComplianceItemEndpoint.cs new file mode 100644 index 0000000..34ed912 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateComplianceItemEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateIssueEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateIssueEndpoint.cs new file mode 100644 index 0000000..aeae4ac --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateIssueEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateOutcomeEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateOutcomeEndpoint.cs new file mode 100644 index 0000000..4caf386 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateOutcomeEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateSiteVisitEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateSiteVisitEndpoint.cs new file mode 100644 index 0000000..917a206 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateSiteVisitEndpoint.cs @@ -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() => 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(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs index f1a1821..23149ff 100644 --- a/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs +++ b/PostFundManagement.Application/Abstractions/Services/ICommunicationsService.cs @@ -26,7 +26,7 @@ public interface ICommunicationsService [Log] [Audit] - Task UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default); + Task UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default); [Log] Task> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default); diff --git a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs index 98b9624..fc3a708 100644 --- a/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IGovernanceService.cs @@ -31,7 +31,7 @@ public interface IGovernanceService [Log] [Audit] - Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default); + Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default); [Log] [Audit] diff --git a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs index 023f172..53249bb 100644 --- a/PostFundManagement.Application/Abstractions/Services/IProjectService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IProjectService.cs @@ -105,7 +105,7 @@ public interface IProjectService [Log] [Audit] - Task UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default); + Task UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default); [Log] Task> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default); diff --git a/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs b/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs index a12bbb1..613677d 100644 --- a/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs +++ b/PostFundManagement.Application/Services/Communications/CommunicationsCacheService.cs @@ -135,6 +135,6 @@ public sealed class CommunicationsCacheService(CommunicationsService service, ID public async Task> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.SendInviteAsync(request, userId, cancellationToken); - public async Task UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) => - await service.UpdateNotificationAsync(request, userId, cancellationToken); + public async Task UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default) => + await service.UpdateNotificationAsync(request, cancellationToken); } diff --git a/PostFundManagement.Application/Services/Communications/CommunicationsService.cs b/PostFundManagement.Application/Services/Communications/CommunicationsService.cs index 712583e..d163422 100644 --- a/PostFundManagement.Application/Services/Communications/CommunicationsService.cs +++ b/PostFundManagement.Application/Services/Communications/CommunicationsService.cs @@ -1,4 +1,5 @@ using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Errors; @@ -8,7 +9,8 @@ using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Communications; -public sealed class CommunicationsService(IDbContextFactory contextFactory, ITenantContextService tenantContext) : ICommunicationsService +public sealed class CommunicationsService(IDbContextFactory contextFactory, + HashService hashService, ITenantContextService tenantContext) : ICommunicationsService { public async Task> SendInviteAsync(InviteRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => @@ -23,24 +25,19 @@ public sealed class CommunicationsService(IDbContextFactory>)(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 UpdateNotificationAsync(UpdateNotificationRequest request, Guid userId, CancellationToken cancellationToken = default) => + public async Task UpdateNotificationAsync(UpdateNotificationRequest request, CancellationToken cancellationToken = default) => await Result.Try((Func>)(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> GetNotificationsAsync(string organisationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(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> GetNotificationAsync(string notificationIdHash, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(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()); })); } diff --git a/PostFundManagement.Application/Services/Communications/Records.cs b/PostFundManagement.Application/Services/Communications/Records.cs index b315be1..706df2f 100644 --- a/PostFundManagement.Application/Services/Communications/Records.cs +++ b/PostFundManagement.Application/Services/Communications/Records.cs @@ -3,10 +3,10 @@ using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Application.Services.Communications; -public sealed record UpdateNotificationRequest([Required] long NotificationId, +public sealed record UpdateNotificationRequest([Required] long NotificationId, string? Destination, [Required] NotificationStatus Status, [Required] DateTime SentAt, - [Required][MaxLength(TextLength)] string Error); + [MaxLength(TextLength)] string? Error); public sealed record CreateNotificationRequest([Required] long OrganisationId, [Required] Guid Recipient, diff --git a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs index 2061276..16a7aac 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceCacheService.cs @@ -311,8 +311,8 @@ public sealed class GovernanceCacheService(GovernanceService service, IDistribut return Result.Ok(cached.FromJson()); })); - public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) => - await service.UpdateOrganisationAsync(request, cancellationToken); + public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) => + await service.UpdateOrganisationAsync(request, userId, cancellationToken); public async Task UpdatePortfolioAsync(UpdatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdatePortfolioAsync(request, userId, cancellationToken); diff --git a/PostFundManagement.Application/Services/Governance/GovernanceService.cs b/PostFundManagement.Application/Services/Governance/GovernanceService.cs index 04b5524..74391dc 100644 --- a/PostFundManagement.Application/Services/Governance/GovernanceService.cs +++ b/PostFundManagement.Application/Services/Governance/GovernanceService.cs @@ -10,7 +10,7 @@ using PostFundManagement.Infrastructure.Database; namespace PostFundManagement.Application.Services.Governance; -public sealed class GovernanceService(IDbContextFactory contextFactory, HashService hashService, +public sealed class GovernanceService(IDbContextFactory contextFactory, HashService hashService, ITenantContextService tenantContext) : IGovernanceService { public async Task> GetProgrammesAsync(string portfolioIdHash, CancellationToken cancellationToken = default) => @@ -140,7 +140,7 @@ public sealed class GovernanceService(IDbContextFactory 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, @@ -148,13 +148,18 @@ public sealed class GovernanceService(IDbContextFactory co Type = request.Type, Status = ActivityStatus.Active, CreatedBy = userId, - CreatedAt = DateTime.UtcNow + CreatedAt = DateTime.UtcNow }); - return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.New); + await context.SaveChangesAsync(cancellationToken); + + var mapResult = await CreateTenantOrganisationAsync(hashService.HashEncodeLongId(organisation.Entity.Id).Value, + hashService.HashEncodeLongId(tenantContext.TenantId).Value, userId, cancellationToken); + + return Result.FailIf(mapResult.IsFailed, CreateError.Create("Failed to create organisation")); })); - public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, CancellationToken cancellationToken = default) => + public async Task> UpdateOrganisationAsync(UpdateOrganisationRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); @@ -165,9 +170,17 @@ public sealed class GovernanceService(IDbContextFactory 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 Task CreatePortfolioAsync(CreatePortfolioRequest request, Guid userId, CancellationToken cancellationToken = default) => @@ -255,48 +268,171 @@ public sealed class GovernanceService(IDbContextFactory co .ToResult(portfolio!.Map()); })); - public Task CreateTenantAsync(CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task CreateTenantAsync(CreateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetTenantsAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var requestedName = request.Name.Humanize(LetterCasing.Title).Trim(); - public Task> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (await context.Tenants.AnyAsync(t => t.Name == requestedName, cancellationToken)) + return Result.Fail(ValidationError.Create("A tenant with an equivalent name exists.")); - public Task UpdateTenantAsync(UpdateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + context.Tenants.Add(new Domain.Entities.Tenant + { + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + Name = requestedName, + Status = ActivityStatus.Active + }); - public Task UpdateTenantUserAsync(UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, + CreateError.Create("Failed to create new tenant")); + })); - public Task CreateTenantOrganisationAsync(string organisationIdHash, string tenantIdHash, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetTenantsAsync(CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task UpdateTenantOrganisationAsync(string tenantOrganisationIdHash, ActivityStatus status, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var tenants = await context.Tenants.AsNoTracking() + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); - public Task> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.Ok(tenants?.Select(t => t.Map()).ToArray() ?? []); + })); - public Task> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetTenantAsync(string tenantIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(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 UpdateTenantAsync(UpdateTenantRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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 UpdateTenantUserAsync(UpdateTenantUserRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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 CreateTenantOrganisationAsync(string organisationIdHash, string tenantIdHash, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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 UpdateTenantOrganisationAsync(string tenantOrganisationIdHash, ActivityStatus status, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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> GetTenantOrganisationsAsync(CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(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> GetTenantOrganisationAsync(string tenantOrganisationIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(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()); + })); } diff --git a/PostFundManagement.Application/Services/Governance/Records.cs b/PostFundManagement.Application/Services/Governance/Records.cs index 949e824..3b85c12 100644 --- a/PostFundManagement.Application/Services/Governance/Records.cs +++ b/PostFundManagement.Application/Services/Governance/Records.cs @@ -3,6 +3,8 @@ 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); @@ -11,8 +13,6 @@ public sealed record UpdateTenantRequest([Required] long TenantId, [Required][MaxLength(LabelLength)] string Name, [Required] ActivityStatus Status); -public sealed record CreateTenantRequest([Required] long TenantId, [Required][MaxLength(LabelLength)] string Name); - public sealed record UpdatePortfolioRequest([Required] long PortfolioId, [Required][MaxLength(LabelLength)] string Name, [Required][MaxLength(TextLength)] string Description, @@ -27,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, diff --git a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs index 0660080..9e0e3ed 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectCacheService.cs @@ -614,8 +614,8 @@ public sealed class ProjectCacheService(ProjectService service, IDistributedCach return Result.Ok(cached.FromJson()); })); - public async Task UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => - await service.UpdateBeneficiaryItemAsync(request, userId, cancellationToken); + public async Task UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => + await service.UpdateBeneficiaryAsync(request, userId, cancellationToken); public async Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => await service.UpdateChangeRequestAsync(request, userId, cancellationToken); diff --git a/PostFundManagement.Application/Services/Projects/ProjectService.cs b/PostFundManagement.Application/Services/Projects/ProjectService.cs index 0d615f8..f37bb77 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectService.cs @@ -383,123 +383,443 @@ public sealed class ProjectService(IDbContextFactory conte return Result.Ok(projects.Select(i => i.Map()).ToArray()); })); - public Task CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task CreateChangeRequestAsync(CreateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) + return Result.Fail(ValidationError.Create("Project does not exist")); - public Task> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + 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, + }); - public Task> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, + CreateError.Create("Failed to create change request")); + })); - public Task CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task UpdateChangeRequestAsync(UpdateChangeRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + 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); - public Task> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(changedTotal > 0, UpdateError.Create("No records were changed")); + })); - public Task> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetChangeRequestsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); - public Task CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); - public Task UpdateBeneficiaryItemAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var changeRequests = await context.ChangeRequests.AsNoTracking() + .Where(c => c.ProjectId == decodeResult.Value) + .ToListAsync(cancellationToken); - public Task> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.Ok(changeRequests?.Select(i => i.Map()).ToArray() ?? []); + })); - public Task CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task> GetChangeRequestAsync(string changeRequestIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(changeRequestIdHash); - public Task UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(decodeResult.Errors)); - public Task> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + var changeRequest = await context.ChangeRequests.AsNoTracking().FirstOrDefaultAsync(c => c.Id == decodeResult.Value, cancellationToken); - public Task CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.FailIf(changeRequest is null, NotFoundError.New) + .ToResult(changeRequest!.Map()); + })); - public Task UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task CreateComplianceItemAsync(CreateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + if (await context.Projects.AnyAsync(p => p.Id == request.ProjectId, cancellationToken)) + return Result.Fail(ValidationError.Create("Project does not exist")); - public Task> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + 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() + }); - public Task CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create compliance item")); + })); - public Task UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public async Task UpdateComplianceItemAsync(UpdateComplianceItemRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - public Task> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + 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); - public Task> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + return Result.OkIf(changedTotal > 0, UpdateError.Create("No records changed")); + })); + + public async Task> GetComplianceItemsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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> GetComplianceItemAsync(string complianceItemIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(complianceItemIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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 CreateBeneficiaryAsync(CreateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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 UpdateBeneficiaryAsync(UpdateBeneficiaryRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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> GetBeneficiariesAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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> GetBeneficiaryAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(beneficiaryIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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 CreateIssueAsync(CreateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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 UpdateIssueAsync(UpdateIssueRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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> GetIssuesAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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> GetIssueAsync(string issueIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(issueIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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 CreateOutcomeAsync(CreateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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 UpdateOutcomeAsync(UpdateOutcomeRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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> GetOutcomesAsync(string beneficiaryIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(beneficiaryIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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> GetOutcomeAsync(string outcomeIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(outcomeIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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 CreateSiteVisitAsync(CreateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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 UpdateSiteVisitAsync(UpdateSiteVisitRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(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> GetSiteVisitsAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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> GetSiteVisitAsync(string siteVisitIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var decodeResult = hashService.DecodeLongIdHash(siteVisitIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(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()); + })); } diff --git a/PostFundManagement.Domain/Extensions/Mappers.cs b/PostFundManagement.Domain/Extensions/Mappers.cs index a49bbbe..13808aa 100644 --- a/PostFundManagement.Domain/Extensions/Mappers.cs +++ b/PostFundManagement.Domain/Extensions/Mappers.cs @@ -110,6 +110,22 @@ public static class Mappers UpdatedAt = entity.UpdatedAt }; + public static ChangeRequest Map(this Entities.ChangeRequest entity) => new() + { + ApprovedBy = entity.ApprovedBy, + CreatedAt = entity.CreatedAt, + CreatedBy = entity.CreatedBy, + Id = entity.Id, + Justification = entity.Justification, + ProjectId = entity.ProjectId, + RevisedBudget = entity.RevisedBudget, + RevisedEndedAt = entity.RevisedEndedAt, + Status = entity.Status, + TenantId = entity.TenantId, + Title = entity.Title, + UpdatedAt = entity.UpdatedAt + }; + public static ComplianceItem Map(this Entities.ComplianceItem entity) => new() { Id = entity.Id,